From c8d0ae5b9fdf0aa587fab3733d07494cd484cce0 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 15 Jan 2024 11:49:00 -0500 Subject: [PATCH] [Response Ops][Alerting] Using `alertsClient` for stack monitoring rule types to write default alerts-as-data docs (#174169) Towards https://github.com/elastic/response-ops-team/issues/164 Resolves https://github.com/elastic/kibana/issues/167436 ## Summary * Switches these rule types to use `alertsClient` from alerting framework in favor of the deprecated `alertFactory` * Defines the `default` alert config for these rule types so framework level fields will be written out into the `.alerts-default.alerts-default` index with no rule type specific fields. * Updated terminology from `alert` to `rule` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../alert_as_data_fields.test.ts.snap | 84 +++- .../alerts/ccr_read_exceptions_rule.test.ts | 202 -------- .../server/alerts/cpu_usage_rule.test.ts | 257 ---------- .../server/alerts/disk_usage_rule.test.ts | 180 ------- .../alerts/large_shard_size_rule.test.ts | 176 ------- .../alerts/license_expiration_rule.test.ts | 250 --------- .../server/alerts/memory_usage_rule.test.ts | 291 ----------- .../missing_monitoring_data_rule.test.ts | 248 --------- ...thread_pool_search_rejections_rule.test.ts | 297 ----------- .../thread_pool_write_rejections_rule.test.ts | 297 ----------- .../server/lib/alerts/fetch_status.ts | 4 +- .../plugins/monitoring/server/plugin.test.ts | 8 +- x-pack/plugins/monitoring/server/plugin.ts | 8 +- .../server/routes/api/v1/alerts/enable.ts | 4 +- .../server/{alerts => rules}/alert_helpers.ts | 0 .../{alerts => rules}/base_rule.test.ts | 0 .../server/{alerts => rules}/base_rule.ts | 91 +++- .../rules/ccr_read_exceptions_rule.test.ts | 473 ++++++++++++++++++ .../ccr_read_exceptions_rule.ts | 44 +- .../cluster_health_rule.test.ts | 134 +++-- .../{alerts => rules}/cluster_health_rule.ts | 79 +-- .../server/rules/cpu_usage_rule.test.ts | 334 +++++++++++++ .../{alerts => rules}/cpu_usage_rule.ts | 46 +- .../server/rules/disk_usage_rule.test.ts | 400 +++++++++++++++ .../{alerts => rules}/disk_usage_rule.ts | 42 +- ...lasticsearch_version_mismatch_rule.test.ts | 106 ++-- .../elasticsearch_version_mismatch_rule.ts | 75 +-- .../server/{alerts => rules}/index.ts | 2 +- .../kibana_version_mismatch_rule.test.ts | 106 ++-- .../kibana_version_mismatch_rule.ts | 55 +- .../rules/large_shard_size_rule.test.ts | 345 +++++++++++++ .../large_shard_size_rule.ts | 40 +- .../rules/license_expiration_rule.test.ts | 387 ++++++++++++++ .../license_expiration_rule.ts | 77 +-- .../logstash_version_mismatch_rule.test.ts | 106 ++-- .../logstash_version_mismatch_rule.ts | 75 +-- .../server/rules/memory_usage_rule.test.ts | 399 +++++++++++++++ .../{alerts => rules}/memory_usage_rule.ts | 42 +- .../missing_monitoring_data_rule.test.ts | 307 ++++++++++++ .../missing_monitoring_data_rule.ts | 47 +- .../nodes_changed_rule.test.ts | 261 +++++----- .../{alerts => rules}/nodes_changed_rule.ts | 83 +-- .../rules_factory.test.ts} | 8 +- .../rules_factory.ts} | 18 +- .../thread_pool_rejections_rule_base.ts | 47 +- ...thread_pool_search_rejections_rule.test.ts | 406 +++++++++++++++ .../thread_pool_search_rejections_rule.ts | 0 .../thread_pool_write_rejections_rule.test.ts | 406 +++++++++++++++ .../thread_pool_write_rejections_rule.ts | 0 x-pack/plugins/monitoring/tsconfig.json | 2 + 50 files changed, 4452 insertions(+), 2897 deletions(-) delete mode 100644 x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/alert_helpers.ts (100%) rename x-pack/plugins/monitoring/server/{alerts => rules}/base_rule.test.ts (100%) rename x-pack/plugins/monitoring/server/{alerts => rules}/base_rule.ts (84%) create mode 100644 x-pack/plugins/monitoring/server/rules/ccr_read_exceptions_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/ccr_read_exceptions_rule.ts (90%) rename x-pack/plugins/monitoring/server/{alerts => rules}/cluster_health_rule.test.ts (56%) rename x-pack/plugins/monitoring/server/{alerts => rules}/cluster_health_rule.ts (74%) create mode 100644 x-pack/plugins/monitoring/server/rules/cpu_usage_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/cpu_usage_rule.ts (83%) create mode 100644 x-pack/plugins/monitoring/server/rules/disk_usage_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/disk_usage_rule.ts (86%) rename x-pack/plugins/monitoring/server/{alerts => rules}/elasticsearch_version_mismatch_rule.test.ts (61%) rename x-pack/plugins/monitoring/server/{alerts => rules}/elasticsearch_version_mismatch_rule.ts (70%) rename x-pack/plugins/monitoring/server/{alerts => rules}/index.ts (96%) rename x-pack/plugins/monitoring/server/{alerts => rules}/kibana_version_mismatch_rule.test.ts (60%) rename x-pack/plugins/monitoring/server/{alerts => rules}/kibana_version_mismatch_rule.ts (79%) create mode 100644 x-pack/plugins/monitoring/server/rules/large_shard_size_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/large_shard_size_rule.ts (87%) create mode 100644 x-pack/plugins/monitoring/server/rules/license_expiration_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/license_expiration_rule.ts (76%) rename x-pack/plugins/monitoring/server/{alerts => rules}/logstash_version_mismatch_rule.test.ts (61%) rename x-pack/plugins/monitoring/server/{alerts => rules}/logstash_version_mismatch_rule.ts (70%) create mode 100644 x-pack/plugins/monitoring/server/rules/memory_usage_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/memory_usage_rule.ts (86%) create mode 100644 x-pack/plugins/monitoring/server/rules/missing_monitoring_data_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/missing_monitoring_data_rule.ts (85%) rename x-pack/plugins/monitoring/server/{alerts => rules}/nodes_changed_rule.test.ts (50%) rename x-pack/plugins/monitoring/server/{alerts => rules}/nodes_changed_rule.ts (80%) rename x-pack/plugins/monitoring/server/{alerts/alerts_factory.test.ts => rules/rules_factory.test.ts} (84%) rename x-pack/plugins/monitoring/server/{alerts/alerts_factory.ts => rules/rules_factory.ts} (81%) rename x-pack/plugins/monitoring/server/{alerts => rules}/thread_pool_rejections_rule_base.ts (88%) create mode 100644 x-pack/plugins/monitoring/server/rules/thread_pool_search_rejections_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/thread_pool_search_rejections_rule.ts (100%) create mode 100644 x-pack/plugins/monitoring/server/rules/thread_pool_write_rejections_rule.test.ts rename x-pack/plugins/monitoring/server/{alerts => rules}/thread_pool_write_rejections_rule.ts (100%) diff --git a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap index b3d9e08a16369..c23428b0f15a4 100644 --- a/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap +++ b/x-pack/plugins/alerting/server/integration_tests/__snapshots__/alert_as_data_fields.test.ts.snap @@ -546,33 +546,89 @@ Object { } `; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_cluster_health 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_cluster_health 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_cpu_usage 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_cpu_usage 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_disk_usage 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_disk_usage 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_elasticsearch_version_mismatch 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_elasticsearch_version_mismatch 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_jvm_memory_usage 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_jvm_memory_usage 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_kibana_version_mismatch 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_kibana_version_mismatch 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_license_expiration 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_license_expiration 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_logstash_version_mismatch 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_logstash_version_mismatch 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_missing_monitoring_data 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_missing_monitoring_data 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_nodes_changed 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_nodes_changed 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_thread_pool_search_rejections 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_thread_pool_search_rejections 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_thread_pool_write_rejections 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_alert_thread_pool_write_rejections 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_ccr_read_exceptions 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_ccr_read_exceptions 1`] = ` +Object { + "fieldMap": Object {}, +} +`; -exports[`Alert as data fields checks detect AAD fields changes for: monitoring_shard_size 1`] = `undefined`; +exports[`Alert as data fields checks detect AAD fields changes for: monitoring_shard_size 1`] = ` +Object { + "fieldMap": Object {}, +} +`; exports[`Alert as data fields checks detect AAD fields changes for: observability.rules.custom_threshold 1`] = ` Object { diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts deleted file mode 100644 index 6887f3051d727..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CCRReadExceptionsRule } from './ccr_read_exceptions_rule'; -import { RULE_CCR_READ_EXCEPTIONS } from '../../common/constants'; -import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; -import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; - -type ICCRReadExceptionsRuleMock = CCRReadExceptionsRule & { - defaultParams: { - duration: string; - }; -} & { - actionVariables: Array<{ - name: string; - description: string; - }>; -}; - -const RealDate = Date; - -jest.mock('../lib/alerts/fetch_ccr_read_exceptions', () => ({ - fetchCCRReadExceptions: 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 }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('CCRReadExceptionsRule', () => { - it('should have defaults', () => { - const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; - expect(rule.ruleOptions.id).toBe(RULE_CCR_READ_EXCEPTIONS); - expect(rule.ruleOptions.name).toBe('CCR read exceptions'); - expect(rule.ruleOptions.throttle).toBe('6h'); - expect(rule.ruleOptions.defaultParams).toStrictEqual({ - duration: '1h', - }); - expect(rule.ruleOptions.actionVariables).toStrictEqual([ - { - name: 'remoteCluster', - description: 'The remote cluster experiencing CCR read exceptions.', - }, - { - name: 'followerIndex', - description: 'The follower index reporting CCR read exceptions.', - }, - { - 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', () => { - const FakeDate = function () {}; - FakeDate.prototype.valueOf = () => 1; - - const clusterUuid = 'abc123'; - const clusterName = 'testCluster'; - const nodeId = 'myNodeId'; - const nodeName = 'myNodeName'; - const remoteCluster = 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1'; - const followerIndex = '.follower_index_1'; - const leaderIndex = '.leader_index_1'; - const readExceptions = [ - { - exception: { - type: 'read_exceptions_type_1', - reason: 'read_exceptions_reason_1', - }, - }, - ]; - const stat = { - remoteCluster, - followerIndex, - leaderIndex, - read_exceptions: readExceptions, - clusterUuid, - nodeId, - nodeName, - }; - - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; - - beforeEach(() => { - Date = FakeDate as DateConstructor; - (fetchCCRReadExceptions 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 rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`, - internalShortMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`, - action: `[View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`, - actionPlain: - 'Verify follower and leader index relationships on the affected remote cluster.', - clusterName, - state: 'firing', - remoteCluster, - remoteClusters: remoteCluster, - followerIndex, - followerIndices: followerIndex, - }); - }); - - it('should handle ccs', async () => { - const ccs = 'testCluster'; - (fetchCCRReadExceptions as jest.Mock).mockImplementation(() => { - return [ - { - ...stat, - ccs, - }, - ]; - }); - const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, - internalShortMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`, - action: `[View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, - actionPlain: - 'Verify follower and leader index relationships on the affected remote cluster.', - clusterName, - state: 'firing', - remoteCluster, - remoteClusters: remoteCluster, - followerIndex, - followerIndices: followerIndex, - }); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts deleted file mode 100644 index 6c5858d48e94e..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CpuUsageRule } from './cpu_usage_rule'; -import { RULE_CPU_USAGE } from '../../common/constants'; -import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; -import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; - -const RealDate = Date; - -jest.mock('../lib/alerts/fetch_cpu_usage_node_stats', () => ({ - fetchCpuUsageNodeStats: 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 }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('CpuUsageRule', () => { - it('should have defaults', () => { - const rule = new CpuUsageRule(); - expect(rule.ruleOptions.id).toBe(RULE_CPU_USAGE); - expect(rule.ruleOptions.name).toBe('CPU Usage'); - expect(rule.ruleOptions.throttle).toBe('1d'); - expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); - expect(rule.ruleOptions.actionVariables).toStrictEqual([ - { name: 'node', description: 'The node reporting high cpu 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 cpuUsage = 91; - const stat = { - clusterUuid, - nodeId, - nodeName, - cpuUsage, - }; - - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; - - beforeEach(() => { - // @ts-ignore - Date = FakeDate; - (fetchCpuUsageNodeStats 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 rule = new CpuUsageRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - const count = 1; - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - ccs: undefined, - cluster: { clusterUuid, clusterName }, - cpuUsage, - itemLabel: undefined, - meta: { - clusterUuid, - cpuUsage, - nodeId, - nodeName, - }, - nodeId, - nodeName, - ui: { - isFiring: true, - message: { - text: `Node #start_link${nodeName}#end_link is reporting cpu usage of ${cpuUsage}% at #absolute`, - nextSteps: [ - { - text: '#start_linkCheck hot threads#end_link', - tokens: [ - { - startToken: '#start_link', - endToken: '#end_link', - type: 'docLink', - partialUrl: - '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html', - }, - ], - }, - { - text: '#start_linkCheck long running tasks#end_link', - tokens: [ - { - startToken: '#start_link', - endToken: '#end_link', - type: 'docLink', - partialUrl: - '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tasks.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: `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', - }); - }); - - it('should not fire actions if under threshold', async () => { - (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { - return [ - { - ...stat, - cpuUsage: 1, - }, - ]; - }); - const rule = new CpuUsageRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [], - }); - expect(scheduleActions).not.toHaveBeenCalled(); - }); - - it('should handle ccs', async () => { - const ccs = 'testCluster'; - (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { - return [ - { - ...stat, - ccs, - }, - ]; - }); - const rule = new CpuUsageRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - const count = 1; - expect(scheduleActions).toHaveBeenCalledWith('default', { - 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/disk_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts deleted file mode 100644 index c0b9720422e53..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DiskUsageRule } from './disk_usage_rule'; -import { RULE_DISK_USAGE } from '../../common/constants'; -import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; -import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; - -type IDiskUsageAlertMock = DiskUsageRule & { - defaultParams: { - threshold: number; - duration: string; - }; -} & { - actionVariables: Array<{ - name: string; - description: string; - }>; -}; - -const RealDate = Date; - -jest.mock('../lib/alerts/fetch_disk_usage_node_stats', () => ({ - fetchDiskUsageNodeStats: 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 }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('DiskUsageRule', () => { - it('should have defaults', () => { - const alert = new DiskUsageRule() as IDiskUsageAlertMock; - expect(alert.ruleOptions.id).toBe(RULE_DISK_USAGE); - expect(alert.ruleOptions.name).toBe('Disk Usage'); - expect(alert.ruleOptions.throttle).toBe('1d'); - expect(alert.ruleOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); - expect(alert.ruleOptions.actionVariables).toStrictEqual([ - { name: 'node', description: 'The node reporting high disk 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', () => { - const FakeDate = function () {}; - FakeDate.prototype.valueOf = () => 1; - - const clusterUuid = 'abc123'; - const clusterName = 'testCluster'; - const nodeId = 'myNodeId'; - const nodeName = 'myNodeName'; - const diskUsage = 91; - const stat = { - clusterUuid, - nodeId, - nodeName, - diskUsage, - }; - - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; - - beforeEach(() => { - Date = FakeDate as DateConstructor; - (fetchDiskUsageNodeStats 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 rule = new DiskUsageRule() as IDiskUsageAlertMock; - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - const count = 1; - expect(scheduleActions).toHaveBeenCalledWith('default', { - 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', - }); - }); - - it('should handle ccs', async () => { - const ccs = 'testCluster'; - (fetchDiskUsageNodeStats as jest.Mock).mockImplementation(() => { - return [ - { - ...stat, - ccs, - }, - ]; - }); - const rule = new DiskUsageRule() as IDiskUsageAlertMock; - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - const count = 1; - expect(scheduleActions).toHaveBeenCalledWith('default', { - 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/large_shard_size_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts deleted file mode 100644 index 1784ca8c0df71..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LargeShardSizeRule } from './large_shard_size_rule'; -import { RULE_LARGE_SHARD_SIZE } from '../../common/constants'; -import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; -import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; - -type ILargeShardSizeRuleMock = LargeShardSizeRule & { - defaultParams: { - threshold: number; - duration: string; - }; -} & { - actionVariables: Array<{ - name: string; - description: string; - }>; -}; - -const RealDate = Date; - -jest.mock('../lib/alerts/fetch_index_shard_size', () => ({ - fetchIndexShardSize: 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 }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('LargeShardSizeRule', () => { - it('should have defaults', () => { - const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; - expect(rule.ruleOptions.id).toBe(RULE_LARGE_SHARD_SIZE); - expect(rule.ruleOptions.name).toBe('Shard size'); - expect(rule.ruleOptions.throttle).toBe('12h'); - expect(rule.ruleOptions.defaultParams).toStrictEqual({ - threshold: 55, - indexPattern: '-.*', - }); - expect(rule.ruleOptions.actionVariables).toStrictEqual([ - { name: 'shardIndex', description: 'The index experiencing large average shard size.' }, - { - 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', () => { - const FakeDate = function () {}; - FakeDate.prototype.valueOf = () => 1; - - const shardIndex = 'apm-8.0.0-onboarding-2021.06.30'; - const shardSize = 0; - const clusterUuid = 'abc123'; - const clusterName = 'testCluster'; - const stat = { - shardIndex, - shardSize, - clusterUuid, - }; - - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; - - beforeEach(() => { - Date = FakeDate as DateConstructor; - (fetchIndexShardSize 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 rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`, - internalShortMessage: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`, - action: `[View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`, - actionPlain: 'Investigate indices with large shard sizes.', - clusterName, - state: 'firing', - shardIndex, - shardIndices: shardIndex, - }); - }); - - it('should handle ccs', async () => { - const ccs = 'testCluster'; - (fetchIndexShardSize as jest.Mock).mockImplementation(() => { - return [ - { - ...stat, - ccs, - }, - ]; - }); - const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(scheduleActions).toHaveBeenCalledWith('default', { - internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, - internalShortMessage: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`, - action: `[View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, - actionPlain: 'Investigate indices with large shard sizes.', - clusterName, - state: 'firing', - shardIndex, - shardIndices: shardIndex, - }); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts deleted file mode 100644 index 7943e2dfca973..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LicenseExpirationRule } from './license_expiration_rule'; -import { RULE_LICENSE_EXPIRATION } from '../../common/constants'; -import { AlertSeverity } from '../../common/enums'; -import { fetchLicenses } from '../lib/alerts/fetch_licenses'; -import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; - -const RealDate = Date; - -jest.mock('../lib/alerts/fetch_licenses', () => ({ - fetchLicenses: jest.fn(), -})); -jest.mock('../lib/alerts/fetch_clusters', () => ({ - fetchClusters: jest.fn(), -})); - -jest.mock('../static_globals', () => ({ - Globals: { - app: { - getLogger: () => ({ debug: jest.fn() }), - config: { - ui: { - show_license_expiration: true, - ccs: { enabled: true }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('LicenseExpirationRule', () => { - it('should have defaults', () => { - const rule = new LicenseExpirationRule(); - expect(rule.ruleOptions.id).toBe(RULE_LICENSE_EXPIRATION); - expect(rule.ruleOptions.name).toBe('License expiration'); - expect(rule.ruleOptions.throttle).toBe('1d'); - expect(rule.ruleOptions.actionVariables).toStrictEqual([ - { name: 'expiredDate', description: 'The date when the license expires.' }, - { name: 'clusterName', description: 'The cluster to which the license belong.' }, - { - 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: '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 license = { - status: 'expired', - type: 'gold', - expiryDateMS: 1000 * 60 * 60 * 24 * 59, - clusterUuid, - }; - - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; - - beforeEach(() => { - // @ts-ignore - Date = FakeDate; - (fetchLicenses as jest.Mock).mockImplementation(() => { - return [license]; - }); - (fetchClusters as jest.Mock).mockImplementation(() => { - return [{ clusterUuid, clusterName }]; - }); - }); - - afterEach(() => { - Date = RealDate; - replaceState.mockReset(); - scheduleActions.mockReset(); - getState.mockReset(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should fire actions', async () => { - jest.useFakeTimers().setSystemTime(new Date('2023-03-30T00:00:00.000Z')); - const alert = new LicenseExpirationRule(); - const type = alert.getRuleType(); - await type.executor({ - ...executorOptions, - params: alert.ruleOptions.defaultParams, - } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - cluster: { clusterUuid, clusterName }, - ccs: undefined, - itemLabel: undefined, - meta: { - clusterUuid: 'abc123', - expiryDateMS: 5097600000, - status: 'expired', - type: 'gold', - }, - nodeId: undefined, - nodeName: undefined, - ui: { - isFiring: true, - message: { - text: 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link', - tokens: [ - { - startToken: '#relative', - type: 'time', - isRelative: true, - isAbsolute: false, - timestamp: 5097600000, - }, - { - startToken: '#absolute', - type: 'time', - isAbsolute: true, - isRelative: false, - timestamp: 5097600000, - }, - { - startToken: '#start_link', - endToken: '#end_link', - type: 'link', - url: 'license', - }, - ], - }, - severity: 'danger', - triggeredMS: 1680134400000, - lastCheckedMS: 0, - }, - }, - ], - }); - expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[Please update your license.](elasticsearch/nodes)', - actionPlain: 'Please update your license.', - internalFullMessage: - 'License expiration alert is firing for testCluster. Your license expires in 53 years. [Please update your license.](elasticsearch/nodes)', - internalShortMessage: - 'License expiration alert is firing for testCluster. Your license expires in 53 years. Please update your license.', - clusterName, - expiredDate: '53 years', - state: 'firing', - }); - }); - - it('should not fire actions if the license is not expired', async () => { - (fetchLicenses as jest.Mock).mockImplementation(() => { - return [ - { - status: 'active', - type: 'gold', - expiryDateMS: 1000 * 60 * 60 * 24 * 61, - clusterUuid, - }, - ]; - }); - const rule = new LicenseExpirationRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(replaceState).not.toHaveBeenCalledWith({}); - expect(scheduleActions).not.toHaveBeenCalled(); - }); - - it('should use danger severity for a license expiring soon', async () => { - (fetchLicenses as jest.Mock).mockImplementation(() => { - return [ - { - status: 'active', - type: 'gold', - expiryDateMS: 1000 * 60 * 60 * 24 * 2, - clusterUuid, - }, - ]; - }); - const rule = new LicenseExpirationRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Danger); - }); - - it('should use warning severity for a license expiring in a bit', async () => { - (fetchLicenses as jest.Mock).mockImplementation(() => { - return [ - { - status: 'active', - type: 'gold', - expiryDateMS: 1000 * 60 * 60 * 24 * 31, - clusterUuid, - }, - ]; - }); - const rule = new LicenseExpirationRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Warning); - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts deleted file mode 100644 index ad0cc98c51bb0..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MemoryUsageRule } from './memory_usage_rule'; -import { RULE_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 '@kbn/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 }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('MemoryUsageRule', () => { - it('should have defaults', () => { - const rule = new MemoryUsageRule(); - expect(rule.ruleOptions.id).toBe(RULE_MEMORY_USAGE); - expect(rule.ruleOptions.name).toBe('Memory Usage (JVM)'); - expect(rule.ruleOptions.throttle).toBe('1d'); - expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); - expect(rule.ruleOptions.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(), - alertFactory: { - create: 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 rule = new MemoryUsageRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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 rule = new MemoryUsageRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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 rule = new MemoryUsageRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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/missing_monitoring_data_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts deleted file mode 100644 index 4490e6c51e902..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MissingMonitoringDataRule } from './missing_monitoring_data_rule'; -import { RULE_MISSING_MONITORING_DATA } from '../../common/constants'; -import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; -import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; - -const RealDate = Date; - -jest.mock('../lib/alerts/fetch_missing_monitoring_data', () => ({ - fetchMissingMonitoringData: 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 }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('MissingMonitoringDataRule', () => { - it('should have defaults', () => { - const rule = new MissingMonitoringDataRule(); - expect(rule.ruleOptions.id).toBe(RULE_MISSING_MONITORING_DATA); - expect(rule.ruleOptions.name).toBe('Missing monitoring data'); - expect(rule.ruleOptions.throttle).toBe('6h'); - expect(rule.ruleOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); - expect(rule.ruleOptions.actionVariables).toStrictEqual([ - { name: 'node', description: 'The node missing monitoring data.' }, - { - 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 gapDuration = 3000001; - const missingData = [ - { - nodeId, - nodeName, - clusterUuid, - gapDuration, - }, - ]; - - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; - - beforeEach(() => { - // @ts-ignore - Date = FakeDate; - (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { - return missingData; - }); - (fetchClusters as jest.Mock).mockImplementation(() => { - return [{ clusterUuid, clusterName }]; - }); - }); - - afterEach(() => { - Date = RealDate; - replaceState.mockReset(); - scheduleActions.mockReset(); - getState.mockReset(); - }); - - it('should fire actions', async () => { - const rule = new MissingMonitoringDataRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - const count = 1; - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - ccs: undefined, - cluster: { clusterUuid, clusterName }, - nodeId, - nodeName, - gapDuration, - itemLabel: undefined, - meta: { - clusterUuid, - gapDuration, - limit: 86400000, - nodeId, - nodeName, - }, - ui: { - isFiring: true, - message: { - text: 'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute', - nextSteps: [ - { - text: '#start_linkView all Elasticsearch nodes#end_link', - tokens: [ - { - startToken: '#start_link', - endToken: '#end_link', - type: 'link', - url: 'elasticsearch/nodes', - }, - ], - }, - { - text: 'Verify monitoring settings on the node', - }, - ], - tokens: [ - { - startToken: '#absolute', - type: 'time', - isAbsolute: true, - isRelative: false, - timestamp: 1, - }, - ], - }, - severity: 'danger', - triggeredMS: 1, - lastCheckedMS: 0, - }, - }, - ], - }); - expect(scheduleActions).toHaveBeenCalledWith('default', { - 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 the node is up and running, then double check the monitoring settings.', - clusterName, - count, - state: 'firing', - }); - }); - - it('should not fire actions if under threshold', async () => { - (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { - return [ - { - ...missingData[0], - gapDuration: 1, - }, - ]; - }); - const rule = new MissingMonitoringDataRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [], - }); - expect(scheduleActions).not.toHaveBeenCalled(); - }); - - it('should handle ccs', async () => { - const ccs = 'testCluster'; - (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { - return [ - { - ...missingData[0], - ccs, - }, - ]; - }); - const rule = new MissingMonitoringDataRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.defaultParams, - } as any); - const count = 1; - expect(scheduleActions).toHaveBeenCalledWith('default', { - 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 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/thread_pool_search_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts deleted file mode 100644 index ff0c917684e10..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ThreadPoolSearchRejectionsRule } from './thread_pool_search_rejections_rule'; -import { RULE_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 '@kbn/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 }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('ThreadpoolSearchRejectionsRule', () => { - it('should have defaults', () => { - const rule = new ThreadPoolSearchRejectionsRule(); - expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_SEARCH_REJECTIONS); - expect(rule.ruleOptions.name).toBe('Thread pool search rejections'); - expect(rule.ruleOptions.throttle).toBe('1d'); - expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); - expect(rule.ruleOptions.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(), - alertFactory: { - create: 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 rule = new ThreadPoolSearchRejectionsRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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 rule = new ThreadPoolSearchRejectionsRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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 rule = new ThreadPoolSearchRejectionsRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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_rule.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts deleted file mode 100644 index b321f687e0672..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ThreadPoolWriteRejectionsRule } from './thread_pool_write_rejections_rule'; -import { RULE_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 '@kbn/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 }, - container: { elasticsearch: { enabled: false } }, - }, - }, - }, - }, -})); - -describe('ThreadpoolWriteRejectionsAlert', () => { - it('should have defaults', () => { - const rule = new ThreadPoolWriteRejectionsRule(); - expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_WRITE_REJECTIONS); - expect(rule.ruleOptions.name).toBe(`Thread pool write rejections`); - expect(rule.ruleOptions.throttle).toBe('1d'); - expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); - expect(rule.ruleOptions.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(), - alertFactory: { - create: 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 rule = new ThreadPoolWriteRejectionsRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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 rule = new ThreadPoolWriteRejectionsRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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 rule = new ThreadPoolWriteRejectionsRule(); - const type = rule.getRuleType(); - await type.executor({ - ...executorOptions, - params: rule.ruleOptions.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 9a0830ab5a1a7..9fca196cf8d98 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -7,7 +7,7 @@ import { RulesClient } from '@kbn/alerting-plugin/server'; import { AlertInstanceState } from '../../../common/types/alerts'; -import { AlertsFactory } from '../../alerts'; +import { RulesFactory } from '../../rules'; import { CommonAlertState, CommonAlertFilter, RulesByType } from '../../../common/types/alerts'; import { RULES } from '../../../common/constants'; @@ -18,7 +18,7 @@ export async function fetchStatus( filters: CommonAlertFilter[] = [] ): Promise { const rulesByType = await Promise.all( - (alertTypes || RULES).map(async (type) => AlertsFactory.getByType(type, rulesClient)) + (alertTypes || RULES).map(async (type) => RulesFactory.getByType(type, rulesClient)) ); if (!rulesByType.length) return {}; diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts index 188f1415e69fb..92ea7cf3295d3 100644 --- a/x-pack/plugins/monitoring/server/plugin.test.ts +++ b/x-pack/plugins/monitoring/server/plugin.test.ts @@ -7,7 +7,7 @@ import { coreMock } from '@kbn/core/server/mocks'; import { MonitoringPlugin } from './plugin'; -import { AlertsFactory } from './alerts'; +import { RulesFactory } from './rules'; jest.mock('./es_client/instantiate_client', () => ({ instantiateClient: jest.fn().mockImplementation(() => ({ @@ -71,10 +71,10 @@ describe('Monitoring plugin', () => { expect(plugin['bulkUploader']).not.toBeUndefined(); }); - it('should register all alerts', async () => { - const alerts = AlertsFactory.getAll(); + it('should register all rules', async () => { + const rules = RulesFactory.getAll(); const plugin = new MonitoringPlugin(initializerContext as any); await plugin.setup(coreSetup as any, setupPlugins as any); - expect(setupPlugins.alerting.registerType).toHaveBeenCalledTimes(alerts.length); + expect(setupPlugins.alerting.registerType).toHaveBeenCalledTimes(rules.length); }); }); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 1747855112964..99cc38e44c0e4 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -30,7 +30,7 @@ import { LOGGING_TAG, SAVED_OBJECT_TELEMETRY, } from '../common/constants'; -import { AlertsFactory } from './alerts'; +import { RulesFactory } from './rules'; import { configSchema, createConfig, MonitoringConfig } from './config'; import { instantiateClient } from './es_client/instantiate_client'; import { initBulkUploader } from './kibana_monitoring'; @@ -123,9 +123,9 @@ export class MonitoringPlugin setupPlugins: this.setupPlugins!, }); - const alerts = AlertsFactory.getAll(); - for (const alert of alerts) { - plugins.alerting?.registerType(alert.getRuleType()); + const rules = RulesFactory.getAll(); + for (const rule of rules) { + plugins.alerting?.registerType(rule.getRuleType()); } const config = createConfig(this.initializerContext.config.get>()); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index dad3f7c16c010..326033e61f1c0 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -8,7 +8,7 @@ import { ActionResult } from '@kbn/actions-plugin/server'; import { RuleTypeParams, SanitizedRule } from '@kbn/alerting-plugin/common'; import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants'; -import { AlertsFactory } from '../../../../alerts'; +import { RulesFactory } from '../../../../rules'; import { handleError } from '../../../../lib/errors'; import { MonitoringCore, RouteDependencies } from '../../../../types'; @@ -26,7 +26,7 @@ export function enableAlertsRoute(server: MonitoringCore, npRoute: RouteDependen const infraContext = await context.infra; const actionContext = await context.actions; - const alerts = AlertsFactory.getAll(); + const alerts = RulesFactory.getAll(); if (alerts.length) { const { isSufficientlySecure, hasPermanentEncryptionKey } = npRoute.alerting ?.getSecurityHealth diff --git a/x-pack/plugins/monitoring/server/alerts/alert_helpers.ts b/x-pack/plugins/monitoring/server/rules/alert_helpers.ts similarity index 100% rename from x-pack/plugins/monitoring/server/alerts/alert_helpers.ts rename to x-pack/plugins/monitoring/server/rules/alert_helpers.ts diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts b/x-pack/plugins/monitoring/server/rules/base_rule.test.ts similarity index 100% rename from x-pack/plugins/monitoring/server/alerts/base_rule.test.ts rename to x-pack/plugins/monitoring/server/rules/base_rule.test.ts diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.ts b/x-pack/plugins/monitoring/server/rules/base_rule.ts similarity index 84% rename from x-pack/plugins/monitoring/server/alerts/base_rule.ts rename to x-pack/plugins/monitoring/server/rules/base_rule.ts index 609d4a2182148..b457185704b76 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/base_rule.ts @@ -7,15 +7,23 @@ import { Logger, ElasticsearchClient, DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; import { RuleType, RuleNotifyWhen, RuleExecutorOptions, - Alert, RulesClient, RuleExecutorServices, + DEFAULT_AAD_CONFIG, + AlertsClientError, } from '@kbn/alerting-plugin/server'; -import { Rule, RuleTypeParams, RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common'; +import { + Rule, + RuleTypeParams, + RawAlertInstance, + SanitizedRule, + AlertInstanceContext, +} from '@kbn/alerting-plugin/common'; import { ActionsClient } from '@kbn/actions-plugin/server'; import { parseDuration } from '@kbn/alerting-plugin/common'; import { @@ -78,7 +86,16 @@ export class BaseRule { this.scopedLogger = Globals.app.getLogger(ruleOptions.id); } - public getRuleType(): RuleType { + public getRuleType(): RuleType< + never, + never, + ExecutedState, + AlertInstanceState, + AlertInstanceContext, + 'default', + never, + DefaultAlert + > { const { id, name, actionVariables } = this.ruleOptions; return { id, @@ -95,15 +112,21 @@ export class BaseRule { minimumLicenseRequired: 'basic', isExportable: false, executor: ( - options: RuleExecutorOptions & { - state: ExecutedState; - } + options: RuleExecutorOptions< + never, + ExecutedState, + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + > ): Promise => this.execute(options), category: DEFAULT_APP_CATEGORIES.management.id, producer: 'monitoring', actionVariables: { context: actionVariables, }, + alerts: DEFAULT_AAD_CONFIG, // As there is "[key: string]: unknown;" in CommonAlertParams, // we couldn't figure out a schema for validation and created a follow on issue: // https://github.com/elastic/kibana/issues/153754 @@ -230,13 +253,23 @@ export class BaseRule { services, params, state, - }: RuleExecutorOptions & { - state: ExecutedState; - }): Promise { + }: RuleExecutorOptions< + never, + ExecutedState, + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >): Promise { this.scopedLogger.debug( `Executing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` ); + const { alertsClient } = services; + if (!alertsClient) { + throw new AlertsClientError(); + } + const esClient = services.scopedClusterClient.asCurrentUser; const clusters = await this.fetchClusters(esClient, params as CommonAlertParams); const data = await this.fetchData(params, esClient, clusters); @@ -270,7 +303,12 @@ export class BaseRule { protected async processData( data: AlertData[], clusters: AlertCluster[], - services: RuleExecutorServices, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, state: ExecutedState ) { const currentUTC = +new Date(); @@ -287,9 +325,6 @@ export class BaseRule { for (const node of nodes) { const newAlertStates: AlertNodeState[] = []; // quick fix for now so that non node level alerts will use the cluster id - const instance = services.alertFactory.create( - node.meta.nodeId || node.meta.instanceId || cluster.clusterUuid - ); if (node.shouldFire) { const { meta } = node; @@ -309,13 +344,19 @@ export class BaseRule { nodeState.ui.message = this.getUiMessage(nodeState, node); // store the state of each node in array. newAlertStates.push(nodeState); - } - 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; + + const alertInstanceState = { alertStates: newAlertStates }; + // update the alert's state with the new node states + if (newAlertStates.length) { + const alertId = node.meta.nodeId || node.meta.instanceId || cluster.clusterUuid; + services.alertsClient?.report({ + id: alertId, + actionGroup: 'default', + state: alertInstanceState, + }); + this.executeActions(services, alertId, alertInstanceState, null, cluster); + state.lastExecutedAction = currentUTC; + } } } } @@ -346,8 +387,14 @@ export class BaseRule { } protected executeActions( - instance: Alert, - instanceState: AlertInstanceState | AlertState | unknown, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, + alertState: AlertInstanceState | AlertState | unknown, item: AlertData | unknown, cluster?: AlertCluster | unknown ) { diff --git a/x-pack/plugins/monitoring/server/rules/ccr_read_exceptions_rule.test.ts b/x-pack/plugins/monitoring/server/rules/ccr_read_exceptions_rule.test.ts new file mode 100644 index 0000000000000..806c382cb3707 --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/ccr_read_exceptions_rule.test.ts @@ -0,0 +1,473 @@ +/* + * 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 { CCRReadExceptionsRule } from './ccr_read_exceptions_rule'; +import { RULE_CCR_READ_EXCEPTIONS } from '../../common/constants'; +import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +type ICCRReadExceptionsRuleMock = CCRReadExceptionsRule & { + defaultParams: { + duration: string; + }; +} & { + actionVariables: Array<{ + name: string; + description: string; + }>; +}; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_ccr_read_exceptions', () => ({ + fetchCCRReadExceptions: 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 }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('CCRReadExceptionsRule', () => { + it('should have defaults', () => { + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + expect(rule.ruleOptions.id).toBe(RULE_CCR_READ_EXCEPTIONS); + expect(rule.ruleOptions.name).toBe('CCR read exceptions'); + expect(rule.ruleOptions.throttle).toBe('6h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ + duration: '1h', + }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ + { + name: 'remoteCluster', + description: 'The remote cluster experiencing CCR read exceptions.', + }, + { + name: 'followerIndex', + description: 'The follower index reporting CCR read exceptions.', + }, + { + 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', () => { + const FakeDate = function () {}; + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const nodeId = 'myNodeId'; + const nodeName = 'myNodeName'; + const remoteCluster = 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1'; + const followerIndex = '.follower_index_1'; + const leaderIndex = '.leader_index_1'; + const readExceptions = [ + { + exception: { + type: 'read_exceptions_type_1', + reason: 'read_exceptions_reason_1', + }, + }, + ]; + const stat = { + remoteCluster, + followerIndex, + leaderIndex, + read_exceptions: readExceptions, + clusterUuid, + nodeId, + nodeName, + }; + + const services = alertsMock.createRuleExecutorServices(); + + const executorOptions = { + services, + state: {}, + }; + + beforeEach(() => { + Date = FakeDate as DateConstructor; + (fetchCCRReadExceptions as jest.Mock).mockImplementation(() => { + return [stat]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + it('should fire action', async () => { + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'default', + id: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1', + state: { + alertStates: [ + { + ccs: undefined, + cluster: { clusterName: 'testCluster', clusterUuid: 'abc123' }, + itemLabel: '.follower_index_1', + meta: { + followerIndex: '.follower_index_1', + instanceId: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1', + itemLabel: '.follower_index_1', + lastReadException: undefined, + leaderIndex: '.leader_index_1', + remoteCluster: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1', + shardId: undefined, + }, + nodeId: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1', + nodeName: '.follower_index_1', + ui: { + isFiring: true, + lastCheckedMS: 0, + message: { + code: undefined, + nextSteps: [ + { + text: '#start_linkIdentify CCR usage/stats#end_link', + tokens: [ + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/ccr', + }, + ], + }, + { + text: '#start_linkManage CCR follower indices#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{basePath}management/data/cross_cluster_replication/follower_indices', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkCreate auto-follow patterns#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{basePath}management/data/cross_cluster_replication/auto_follow_patterns', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkAdd follower index API (Docs)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/ccr-put-follow.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkCross-cluster replication (Docs)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/xpack-ccr.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkBi-directional replication (Blog)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}blog/bi-directional-replication-with-elasticsearch-cross-cluster-replication-ccr', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkFollow the Leader (Blog)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}blog/follow-the-leader-an-introduction-to-cross-cluster-replication-in-elasticsearch', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + ], + text: 'Follower index #start_link.follower_index_1#end_link is reporting CCR read exceptions on remote cluster: BcK-0pmsQniyPQfZuauuXw_remote_cluster_1 at #absolute', + tokens: [ + { + isAbsolute: true, + isRelative: false, + startToken: '#absolute', + timestamp: 1, + type: 'time', + }, + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/ccr/.follower_index_1/shard/undefined', + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1', + context: { + internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`, + action: `[View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: + 'Verify follower and leader index relationships on the affected remote cluster.', + clusterName, + state: 'firing', + remoteCluster, + remoteClusters: remoteCluster, + followerIndex, + followerIndices: followerIndex, + }, + payload: { + [ALERT_REASON]: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`, + }, + }); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchCCRReadExceptions as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + ccs, + }, + ]; + }); + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'default', + id: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1', + state: { + alertStates: [ + { + ccs: 'testCluster', + cluster: { clusterName: 'testCluster', clusterUuid: 'abc123' }, + itemLabel: '.follower_index_1', + meta: { + followerIndex: '.follower_index_1', + instanceId: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1', + itemLabel: '.follower_index_1', + lastReadException: undefined, + leaderIndex: '.leader_index_1', + remoteCluster: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1', + shardId: undefined, + }, + nodeId: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1', + nodeName: '.follower_index_1', + ui: { + isFiring: true, + lastCheckedMS: 0, + message: { + code: undefined, + nextSteps: [ + { + text: '#start_linkIdentify CCR usage/stats#end_link', + tokens: [ + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/ccr', + }, + ], + }, + { + text: '#start_linkManage CCR follower indices#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{basePath}management/data/cross_cluster_replication/follower_indices', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkCreate auto-follow patterns#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{basePath}management/data/cross_cluster_replication/auto_follow_patterns', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkAdd follower index API (Docs)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/ccr-put-follow.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkCross-cluster replication (Docs)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/xpack-ccr.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkBi-directional replication (Blog)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}blog/bi-directional-replication-with-elasticsearch-cross-cluster-replication-ccr', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkFollow the Leader (Blog)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}blog/follow-the-leader-an-introduction-to-cross-cluster-replication-in-elasticsearch', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + ], + text: 'Follower index #start_link.follower_index_1#end_link is reporting CCR read exceptions on remote cluster: BcK-0pmsQniyPQfZuauuXw_remote_cluster_1 at #absolute', + tokens: [ + { + isAbsolute: true, + isRelative: false, + startToken: '#absolute', + timestamp: 1, + type: 'time', + }, + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/ccr/.follower_index_1/shard/undefined', + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'BcK-0pmsQniyPQfZuauuXw_remote_cluster_1:.follower_index_1', + context: { + internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, + internalShortMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`, + action: `[View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, + actionPlain: + 'Verify follower and leader index relationships on the affected remote cluster.', + clusterName, + state: 'firing', + remoteCluster, + remoteClusters: remoteCluster, + followerIndex, + followerIndices: followerIndex, + }, + payload: { + [ALERT_REASON]: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Verify follower and leader index relationships on the affected remote cluster.`, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts b/x-pack/plugins/monitoring/server/rules/ccr_read_exceptions_rule.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts rename to x-pack/plugins/monitoring/server/rules/ccr_read_exceptions_rule.ts index 1c1bfe7bf3018..70ddc7a498fd2 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/ccr_read_exceptions_rule.ts @@ -7,9 +7,11 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration'; -import { SanitizedRule, RawAlertInstance } from '@kbn/alerting-plugin/common'; +import { SanitizedRule, RawAlertInstance, AlertInstanceContext } from '@kbn/alerting-plugin/common'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -209,7 +211,13 @@ export class CCRReadExceptionsRule extends BaseRule { } protected executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -261,21 +269,27 @@ export class CCRReadExceptionsRule extends BaseRule { } ); - instance.scheduleActions('default', { - internalShortMessage, - internalFullMessage, - state: AlertingDefaults.ALERT_STATE.firing, - remoteCluster, - followerIndex, - /* continue to send "remoteClusters" and "followerIndices" values for users still using it though + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + remoteCluster, + followerIndex, + /* continue to send "remoteClusters" and "followerIndices" values for users still using it though we have replaced it with "remoteCluster" and "followerIndex" in the template due to alerts per index instead of all indices see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ - remoteClusters: remoteCluster, - followerIndices: followerIndex, - clusterName: cluster.clusterName, - action, - actionPlain: shortActionText, + remoteClusters: remoteCluster, + followerIndices: followerIndex, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts b/x-pack/plugins/monitoring/server/rules/cluster_health_rule.test.ts similarity index 56% rename from x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts rename to x-pack/plugins/monitoring/server/rules/cluster_health_rule.test.ts index d67f10613607a..fe1d8562b7ed4 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts +++ b/x-pack/plugins/monitoring/server/rules/cluster_health_rule.test.ts @@ -10,7 +10,8 @@ import { RULE_CLUSTER_HEALTH } from '../../common/constants'; import { AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; const RealDate = Date; @@ -75,24 +76,8 @@ describe('ClusterHealthRule', () => { }, ]; - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; beforeEach(() => { // @ts-ignore @@ -107,66 +92,77 @@ describe('ClusterHealthRule', () => { afterEach(() => { Date = RealDate; - replaceState.mockReset(); - scheduleActions.mockReset(); - getState.mockReset(); + jest.resetAllMocks(); }); - it('should fire actions', async () => { + it('should fire action', async () => { const rule = new ClusterHealthRule(); const type = rule.getRuleType(); await type.executor({ ...executorOptions, params: {}, } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs, - itemLabel: undefined, - nodeId: undefined, - nodeName: undefined, - meta: { + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs, - clusterUuid, - health: AlertClusterHealthType.Yellow, - }, - ui: { - isFiring: true, - message: { - text: 'Elasticsearch cluster health is yellow.', - nextSteps: [ - { - text: 'Allocate missing replica shards. #start_linkView now#end_link', - tokens: [ - { - startToken: '#start_link', - endToken: '#end_link', - type: 'link', - url: 'elasticsearch/indices', - }, - ], - }, - ], + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + health: AlertClusterHealthType.Yellow, + }, + ui: { + isFiring: true, + message: { + text: 'Elasticsearch cluster health is yellow.', + nextSteps: [ + { + text: 'Allocate missing replica shards. #start_linkView now#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/indices', + }, + ], + }, + ], + }, + severity: AlertSeverity.Warning, + triggeredMS: 1, + lastCheckedMS: 0, }, - severity: AlertSeverity.Warning, - triggeredMS: 1, - lastCheckedMS: 0, }, - }, - ], + ], + }, }); - expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[Allocate missing replica shards.](elasticsearch/indices)', - actionPlain: 'Allocate missing replica shards.', - internalFullMessage: - 'Cluster health alert is firing for testCluster. Current health is yellow. [Allocate missing replica shards.](elasticsearch/indices)', - internalShortMessage: - 'Cluster health alert is firing for testCluster. Current health is yellow. Allocate missing replica shards.', - clusterName, - clusterHealth: 'yellow', - state: 'firing', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + action: '[Allocate missing replica shards.](elasticsearch/indices)', + actionPlain: 'Allocate missing replica shards.', + internalFullMessage: + 'Cluster health alert is firing for testCluster. Current health is yellow. [Allocate missing replica shards.](elasticsearch/indices)', + internalShortMessage: + 'Cluster health alert is firing for testCluster. Current health is yellow. Allocate missing replica shards.', + clusterName, + clusterHealth: 'yellow', + state: 'firing', + }, + payload: { + [ALERT_REASON]: + 'Cluster health alert is firing for testCluster. Current health is yellow. Allocate missing replica shards.', + }, }); }); @@ -186,8 +182,8 @@ describe('ClusterHealthRule', () => { ...executorOptions, params: {}, } as any); - expect(replaceState).not.toHaveBeenCalledWith({}); - expect(scheduleActions).not.toHaveBeenCalled(); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts b/x-pack/plugins/monitoring/server/rules/cluster_health_rule.ts similarity index 74% rename from x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts rename to x-pack/plugins/monitoring/server/rules/cluster_health_rule.ts index f93d182684560..f603be7f3b622 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/cluster_health_rule.ts @@ -7,8 +7,10 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -115,8 +117,14 @@ export class ClusterHealthRule extends BaseRule { }; } - protected async executeActions( - instance: Alert, + protected executeActions( + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -139,34 +147,41 @@ export class ClusterHealthRule extends BaseRule { }); const action = `[${actionText}](elasticsearch/indices)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, - values: { - clusterName: cluster.clusterName, - health, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, - values: { - clusterName: cluster.clusterName, - health, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterHealth: health, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, + values: { + clusterName: cluster.clusterName, + health, + actionText, + }, + } + ); + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, + values: { + clusterName: cluster.clusterName, + health, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterHealth: health, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/rules/cpu_usage_rule.test.ts b/x-pack/plugins/monitoring/server/rules/cpu_usage_rule.test.ts new file mode 100644 index 0000000000000..1b107d4031981 --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/cpu_usage_rule.test.ts @@ -0,0 +1,334 @@ +/* + * 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 { CpuUsageRule } from './cpu_usage_rule'; +import { RULE_CPU_USAGE } from '../../common/constants'; +import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_cpu_usage_node_stats', () => ({ + fetchCpuUsageNodeStats: 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 }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('CpuUsageRule', () => { + it('should have defaults', () => { + const rule = new CpuUsageRule(); + expect(rule.ruleOptions.id).toBe(RULE_CPU_USAGE); + expect(rule.ruleOptions.name).toBe('CPU Usage'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ + { name: 'node', description: 'The node reporting high cpu 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 cpuUsage = 91; + const stat = { + clusterUuid, + nodeId, + nodeName, + cpuUsage, + }; + + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { + services, + state: {}, + }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [stat]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + it('should fire action', async () => { + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'default', + id: 'myNodeId', + state: { + alertStates: [ + { + ccs: undefined, + cluster: { clusterUuid, clusterName }, + cpuUsage, + itemLabel: undefined, + meta: { + clusterUuid, + cpuUsage, + nodeId, + nodeName, + }, + nodeId, + nodeName, + ui: { + isFiring: true, + message: { + text: `Node #start_link${nodeName}#end_link is reporting cpu usage of ${cpuUsage}% at #absolute`, + nextSteps: [ + { + text: '#start_linkCheck hot threads#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html', + }, + ], + }, + { + text: '#start_linkCheck long running tasks#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tasks.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(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'myNodeId', + context: { + 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', + }, + payload: { + [ALERT_REASON]: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`, + }, + }); + }); + + it('should not fire actions if under threshold', async () => { + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + cpuUsage: 1, + }, + ]; + }); + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchCpuUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + ccs, + }, + ]; + }); + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + actionGroup: 'default', + id: 'myNodeId', + state: { + alertStates: [ + { + ccs: 'testCluster', + cluster: { clusterUuid, clusterName }, + cpuUsage, + itemLabel: undefined, + meta: { + ccs: 'testCluster', + clusterUuid, + cpuUsage, + nodeId, + nodeName, + }, + nodeId, + nodeName, + ui: { + isFiring: true, + message: { + text: `Node #start_link${nodeName}#end_link is reporting cpu usage of ${cpuUsage}% at #absolute`, + nextSteps: [ + { + text: '#start_linkCheck hot threads#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/cluster-nodes-hot-threads.html', + }, + ], + }, + { + text: '#start_linkCheck long running tasks#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'docLink', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tasks.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(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'myNodeId', + context: { + 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', + }, + payload: { + [ALERT_REASON]: `CPU usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify CPU level of node.`, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts b/x-pack/plugins/monitoring/server/rules/cpu_usage_rule.ts similarity index 83% rename from x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts rename to x-pack/plugins/monitoring/server/rules/cpu_usage_rule.ts index 92c45c9e61ae2..be8aad8bf01a4 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/cpu_usage_rule.ts @@ -8,9 +8,11 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { AlertInstanceContext, RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common'; import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -144,7 +146,13 @@ export class CpuUsageRule extends BaseRule { } protected executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -191,19 +199,25 @@ export class CpuUsageRule extends BaseRule { }, } ); - 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, + services.alertsClient?.setAlertData({ + id: alertId, + context: { + 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, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/rules/disk_usage_rule.test.ts b/x-pack/plugins/monitoring/server/rules/disk_usage_rule.test.ts new file mode 100644 index 0000000000000..5e1c30dce78f6 --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/disk_usage_rule.test.ts @@ -0,0 +1,400 @@ +/* + * 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 { DiskUsageRule } from './disk_usage_rule'; +import { RULE_DISK_USAGE } from '../../common/constants'; +import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +type IDiskUsageAlertMock = DiskUsageRule & { + defaultParams: { + threshold: number; + duration: string; + }; +} & { + actionVariables: Array<{ + name: string; + description: string; + }>; +}; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_disk_usage_node_stats', () => ({ + fetchDiskUsageNodeStats: 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 }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('DiskUsageRule', () => { + it('should have defaults', () => { + const alert = new DiskUsageRule() as IDiskUsageAlertMock; + expect(alert.ruleOptions.id).toBe(RULE_DISK_USAGE); + expect(alert.ruleOptions.name).toBe('Disk Usage'); + expect(alert.ruleOptions.throttle).toBe('1d'); + expect(alert.ruleOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); + expect(alert.ruleOptions.actionVariables).toStrictEqual([ + { name: 'node', description: 'The node reporting high disk 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', () => { + const FakeDate = function () {}; + FakeDate.prototype.valueOf = () => 1; + + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const nodeId = 'myNodeId'; + const nodeName = 'myNodeName'; + const diskUsage = 91; + const stat = { + clusterUuid, + nodeId, + nodeName, + diskUsage, + }; + + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; + + beforeEach(() => { + Date = FakeDate as DateConstructor; + (fetchDiskUsageNodeStats as jest.Mock).mockImplementation(() => { + return [stat]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + it('should fire action', async () => { + const rule = new DiskUsageRule() as IDiskUsageAlertMock; + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'myNodeId', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: undefined, + cluster: { + clusterName: 'testCluster', + clusterUuid: 'abc123', + }, + diskUsage: 91, + itemLabel: undefined, + meta: { + clusterUuid: 'abc123', + diskUsage: 91, + nodeId: 'myNodeId', + nodeName: 'myNodeName', + }, + nodeId: 'myNodeId', + nodeName: 'myNodeName', + ui: { + isFiring: true, + lastCheckedMS: 0, + message: { + nextSteps: [ + { + text: '#start_linkTune for disk usage#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tune-for-disk-usage.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkIdentify large indices#end_link', + tokens: [ + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/indices', + }, + ], + }, + { + text: '#start_linkImplement ILM policies#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/index-lifecycle-management.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkAdd more data nodes#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkResize your deployment (ECE)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + ], + text: 'Node #start_linkmyNodeName#end_link is reporting disk usage of 91% at #absolute', + tokens: [ + { + isAbsolute: true, + isRelative: false, + startToken: '#absolute', + timestamp: 1, + type: 'time', + }, + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/nodes/myNodeId', + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'myNodeId', + context: { + 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', + }, + payload: { + [ALERT_REASON]: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`, + }, + }); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchDiskUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + ccs, + }, + ]; + }); + const rule = new DiskUsageRule() as IDiskUsageAlertMock; + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'myNodeId', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: 'testCluster', + cluster: { + clusterName: 'testCluster', + clusterUuid: 'abc123', + }, + diskUsage: 91, + itemLabel: undefined, + meta: { + ccs: 'testCluster', + clusterUuid: 'abc123', + diskUsage: 91, + nodeId: 'myNodeId', + nodeName: 'myNodeName', + }, + nodeId: 'myNodeId', + nodeName: 'myNodeName', + ui: { + isFiring: true, + lastCheckedMS: 0, + message: { + nextSteps: [ + { + text: '#start_linkTune for disk usage#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/tune-for-disk-usage.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkIdentify large indices#end_link', + tokens: [ + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/indices', + }, + ], + }, + { + text: '#start_linkImplement ILM policies#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/index-lifecycle-management.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkAdd more data nodes#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/{docLinkVersion}/add-elasticsearch-nodes.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkResize your deployment (ECE)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/cloud-enterprise/current/ece-resize-deployment.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + ], + text: 'Node #start_linkmyNodeName#end_link is reporting disk usage of 91% at #absolute', + tokens: [ + { + isAbsolute: true, + isRelative: false, + startToken: '#absolute', + timestamp: 1, + type: 'time', + }, + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/nodes/myNodeId', + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'myNodeId', + context: { + 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', + }, + payload: { + [ALERT_REASON]: `Disk usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify disk usage level of node.`, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts b/x-pack/plugins/monitoring/server/rules/disk_usage_rule.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts rename to x-pack/plugins/monitoring/server/rules/disk_usage_rule.ts index 77a5e0e8bd5bc..a57de61e9bd66 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/disk_usage_rule.ts @@ -8,8 +8,10 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { AlertInstanceContext, RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -151,7 +153,13 @@ export class DiskUsageRule extends BaseRule { } protected executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -200,19 +208,25 @@ export class DiskUsageRule extends BaseRule { } ); - 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 + services.alertsClient?.setAlertData({ + id: alertId, + context: { + 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, + nodes: `${firingNode.nodeName}:${firingNode.diskUsage}`, + count: 1, + node: `${firingNode.nodeName}:${firingNode.diskUsage}`, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/rules/elasticsearch_version_mismatch_rule.test.ts similarity index 61% rename from x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts rename to x-pack/plugins/monitoring/server/rules/elasticsearch_version_mismatch_rule.test.ts index 147f3435197a4..eafb51136fc73 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/rules/elasticsearch_version_mismatch_rule.test.ts @@ -9,7 +9,8 @@ import { ElasticsearchVersionMismatchRule } from './elasticsearch_version_mismat import { RULE_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; const RealDate = Date; @@ -79,24 +80,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { }, ]; - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; beforeEach(() => { // @ts-ignore @@ -111,52 +96,63 @@ describe('ElasticsearchVersionMismatchAlert', () => { afterEach(() => { Date = RealDate; - replaceState.mockReset(); - scheduleActions.mockReset(); - getState.mockReset(); + jest.resetAllMocks(); }); - it('should fire actions', async () => { + it('should fire action', async () => { const rule = new ElasticsearchVersionMismatchRule(); const type = rule.getRuleType(); await type.executor({ ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs, - itemLabel: undefined, - nodeId: undefined, - nodeName: undefined, - meta: { + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs, - clusterUuid, - versions: ['8.0.0', '7.2.1'], - }, - ui: { - isFiring: true, - message: { - text: 'Multiple versions of Elasticsearch (8.0.0, 7.2.1) running in this cluster.', + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, + ui: { + isFiring: true, + message: { + text: 'Multiple versions of Elasticsearch (8.0.0, 7.2.1) running in this cluster.', + }, + severity: 'warning', + triggeredMS: 1, + lastCheckedMS: 0, }, - severity: 'warning', - triggeredMS: 1, - lastCheckedMS: 0, }, - }, - ], + ], + }, }); - expect(scheduleActions).toHaveBeenCalledWith('default', { - action: `[View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, - actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: `Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, - internalShortMessage: - 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: ['8.0.0', '7.2.1'], - clusterName, - state: 'firing', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify you have the same version across all nodes.', + internalFullMessage: `Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: + 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', + versionList: ['8.0.0', '7.2.1'], + clusterName, + state: 'firing', + }, + payload: { + [ALERT_REASON]: + 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', + }, }); }); @@ -176,8 +172,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).not.toHaveBeenCalledWith({}); - expect(scheduleActions).not.toHaveBeenCalled(); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/rules/elasticsearch_version_mismatch_rule.ts similarity index 70% rename from x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts rename to x-pack/plugins/monitoring/server/rules/elasticsearch_version_mismatch_rule.ts index 77ad2dc5f3aa2..a2c4f4aa6687f 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/elasticsearch_version_mismatch_rule.ts @@ -7,8 +7,10 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -87,7 +89,13 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -118,33 +126,40 @@ export class ElasticsearchVersionMismatchRule extends BaseRule { state.ccs ); const action = `[${fullActionText}](${globalStateLink})`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions: versions.join(', '), - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ); + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/rules/index.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/index.ts rename to x-pack/plugins/monitoring/server/rules/index.ts index d5c59c802b681..2be1e70d5f15e 100644 --- a/x-pack/plugins/monitoring/server/alerts/index.ts +++ b/x-pack/plugins/monitoring/server/rules/index.ts @@ -20,4 +20,4 @@ export { NodesChangedRule } from './nodes_changed_rule'; export { ElasticsearchVersionMismatchRule } from './elasticsearch_version_mismatch_rule'; export { KibanaVersionMismatchRule } from './kibana_version_mismatch_rule'; export { LogstashVersionMismatchRule } from './logstash_version_mismatch_rule'; -export { AlertsFactory } from './alerts_factory'; +export { RulesFactory } from './rules_factory'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/rules/kibana_version_mismatch_rule.test.ts similarity index 60% rename from x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts rename to x-pack/plugins/monitoring/server/rules/kibana_version_mismatch_rule.test.ts index d7032597881a3..46b777dbee4a2 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/rules/kibana_version_mismatch_rule.test.ts @@ -9,7 +9,8 @@ import { KibanaVersionMismatchRule } from './kibana_version_mismatch_rule'; import { RULE_KIBANA_VERSION_MISMATCH } from '../../common/constants'; import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; const RealDate = Date; @@ -82,24 +83,8 @@ describe('KibanaVersionMismatchRule', () => { }, ]; - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; beforeEach(() => { // @ts-ignore @@ -114,52 +99,63 @@ describe('KibanaVersionMismatchRule', () => { afterEach(() => { Date = RealDate; - replaceState.mockReset(); - scheduleActions.mockReset(); - getState.mockReset(); + jest.resetAllMocks(); }); - it('should fire actions', async () => { + it('should fire action', async () => { const rule = new KibanaVersionMismatchRule(); const type = rule.getRuleType(); await type.executor({ ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs, - itemLabel: undefined, - nodeId: undefined, - nodeName: undefined, - meta: { + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs, - clusterUuid, - versions: ['8.0.0', '7.2.1'], - }, - ui: { - isFiring: true, - message: { - text: 'Multiple versions of Kibana (8.0.0, 7.2.1) running in this cluster.', + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, + ui: { + isFiring: true, + message: { + text: 'Multiple versions of Kibana (8.0.0, 7.2.1) running in this cluster.', + }, + severity: 'warning', + triggeredMS: 1, + lastCheckedMS: 0, }, - severity: 'warning', - triggeredMS: 1, - lastCheckedMS: 0, }, - }, - ], + ], + }, }); - expect(scheduleActions).toHaveBeenCalledWith('default', { - action: `[View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, - actionPlain: 'Verify you have the same version across all instances.', - internalFullMessage: `Kibana version mismatch alert is firing for testCluster. Kibana is running 8.0.0, 7.2.1. [View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, - internalShortMessage: - 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', - versionList: ['8.0.0', '7.2.1'], - clusterName, - state: 'firing', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + action: `[View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify you have the same version across all instances.', + internalFullMessage: `Kibana version mismatch alert is firing for testCluster. Kibana is running 8.0.0, 7.2.1. [View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: + 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', + versionList: ['8.0.0', '7.2.1'], + clusterName, + state: 'firing', + }, + payload: { + [ALERT_REASON]: + 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', + }, }); }); @@ -179,8 +175,8 @@ describe('KibanaVersionMismatchRule', () => { ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).not.toHaveBeenCalledWith({}); - expect(scheduleActions).not.toHaveBeenCalled(); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/rules/kibana_version_mismatch_rule.ts similarity index 79% rename from x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts rename to x-pack/plugins/monitoring/server/rules/kibana_version_mismatch_rule.ts index ff7653dc59e85..559c27f69d46f 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/kibana_version_mismatch_rule.ts @@ -7,8 +7,10 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -97,7 +99,13 @@ export class KibanaVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -128,6 +136,16 @@ export class KibanaVersionMismatchRule extends BaseRule { state.ccs ); const action = `[${fullActionText}](${globalStateLink})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ); const internalFullMessage = i18n.translate( 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', { @@ -139,23 +157,20 @@ export class KibanaVersionMismatchRule extends BaseRule { }, } ); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage, - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/rules/large_shard_size_rule.test.ts b/x-pack/plugins/monitoring/server/rules/large_shard_size_rule.test.ts new file mode 100644 index 0000000000000..0ad6c7687c203 --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/large_shard_size_rule.test.ts @@ -0,0 +1,345 @@ +/* + * 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 { LargeShardSizeRule } from './large_shard_size_rule'; +import { RULE_LARGE_SHARD_SIZE } from '../../common/constants'; +import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +type ILargeShardSizeRuleMock = LargeShardSizeRule & { + defaultParams: { + threshold: number; + duration: string; + }; +} & { + actionVariables: Array<{ + name: string; + description: string; + }>; +}; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_index_shard_size', () => ({ + fetchIndexShardSize: 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 }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('LargeShardSizeRule', () => { + it('should have defaults', () => { + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + expect(rule.ruleOptions.id).toBe(RULE_LARGE_SHARD_SIZE); + expect(rule.ruleOptions.name).toBe('Shard size'); + expect(rule.ruleOptions.throttle).toBe('12h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ + threshold: 55, + indexPattern: '-.*', + }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ + { name: 'shardIndex', description: 'The index experiencing large average shard size.' }, + { + 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', () => { + const FakeDate = function () {}; + FakeDate.prototype.valueOf = () => 1; + + const shardIndex = 'apm-8.0.0-onboarding-2021.06.30'; + const shardSize = 0; + const clusterUuid = 'abc123'; + const clusterName = 'testCluster'; + const stat = { + shardIndex, + shardSize, + clusterUuid, + }; + + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; + + beforeEach(() => { + Date = FakeDate as DateConstructor; + (fetchIndexShardSize as jest.Mock).mockImplementation(() => { + return [stat]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + it('should fire action', async () => { + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123:apm-8.0.0-onboarding-2021.06.30', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: undefined, + cluster: { + clusterName: 'testCluster', + clusterUuid: 'abc123', + }, + itemLabel: 'apm-8.0.0-onboarding-2021.06.30', + meta: { + instanceId: 'abc123:apm-8.0.0-onboarding-2021.06.30', + itemLabel: 'apm-8.0.0-onboarding-2021.06.30', + shardIndex: 'apm-8.0.0-onboarding-2021.06.30', + shardSize: 0, + }, + nodeId: 'abc123:apm-8.0.0-onboarding-2021.06.30', + nodeName: 'apm-8.0.0-onboarding-2021.06.30', + ui: { + isFiring: true, + lastCheckedMS: 0, + message: { + nextSteps: [ + { + text: '#start_linkInvestigate detailed index stats#end_link', + tokens: [ + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/indices/apm-8.0.0-onboarding-2021.06.30/advanced', + }, + ], + }, + { + text: '#start_linkHow to size your shards (Docs)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/current/size-your-shards.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkShard sizing tips (Blog)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + ], + text: 'The following index: #start_linkapm-8.0.0-onboarding-2021.06.30#end_link has a large average shard size of: 0GB at #absolute', + tokens: [ + { + isAbsolute: true, + isRelative: false, + startToken: '#absolute', + timestamp: 1, + type: 'time', + }, + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/indices/apm-8.0.0-onboarding-2021.06.30', + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123:apm-8.0.0-onboarding-2021.06.30', + context: { + internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`, + action: `[View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Investigate indices with large shard sizes.', + clusterName, + state: 'firing', + shardIndex, + shardIndices: shardIndex, + }, + payload: { + [ALERT_REASON]: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`, + }, + }); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchIndexShardSize as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + ccs, + }, + ]; + }); + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123:apm-8.0.0-onboarding-2021.06.30', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: 'testCluster', + cluster: { + clusterName: 'testCluster', + clusterUuid: 'abc123', + }, + itemLabel: 'apm-8.0.0-onboarding-2021.06.30', + meta: { + instanceId: 'abc123:apm-8.0.0-onboarding-2021.06.30', + itemLabel: 'apm-8.0.0-onboarding-2021.06.30', + shardIndex: 'apm-8.0.0-onboarding-2021.06.30', + shardSize: 0, + }, + nodeId: 'abc123:apm-8.0.0-onboarding-2021.06.30', + nodeName: 'apm-8.0.0-onboarding-2021.06.30', + ui: { + isFiring: true, + lastCheckedMS: 0, + message: { + nextSteps: [ + { + text: '#start_linkInvestigate detailed index stats#end_link', + tokens: [ + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/indices/apm-8.0.0-onboarding-2021.06.30/advanced', + }, + ], + }, + { + text: '#start_linkHow to size your shards (Docs)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}guide/en/elasticsearch/reference/current/size-your-shards.html', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + { + text: '#start_linkShard sizing tips (Blog)#end_link', + tokens: [ + { + endToken: '#end_link', + partialUrl: + '{elasticWebsiteUrl}blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster', + startToken: '#start_link', + type: 'docLink', + }, + ], + }, + ], + text: 'The following index: #start_linkapm-8.0.0-onboarding-2021.06.30#end_link has a large average shard size of: 0GB at #absolute', + tokens: [ + { + isAbsolute: true, + isRelative: false, + startToken: '#absolute', + timestamp: 1, + type: 'time', + }, + { + endToken: '#end_link', + startToken: '#start_link', + type: 'link', + url: 'elasticsearch/indices/apm-8.0.0-onboarding-2021.06.30', + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123:apm-8.0.0-onboarding-2021.06.30', + context: { + internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, + internalShortMessage: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`, + action: `[View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, + actionPlain: 'Investigate indices with large shard sizes.', + clusterName, + state: 'firing', + shardIndex, + shardIndices: shardIndex, + }, + payload: { + [ALERT_REASON]: `Large shard size alert is firing for the following index: ${shardIndex}. Investigate indices with large shard sizes.`, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts b/x-pack/plugins/monitoring/server/rules/large_shard_size_rule.ts similarity index 87% rename from x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts rename to x-pack/plugins/monitoring/server/rules/large_shard_size_rule.ts index 528c757299f9c..67664695fec87 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/large_shard_size_rule.ts @@ -7,8 +7,10 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { SanitizedRule, RawAlertInstance } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { SanitizedRule, RawAlertInstance, AlertInstanceContext } from '@kbn/alerting-plugin/common'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -149,7 +151,13 @@ export class LargeShardSizeRule extends BaseRule { } protected executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -196,19 +204,25 @@ export class LargeShardSizeRule extends BaseRule { } ); - instance.scheduleActions('default', { - internalShortMessage, - internalFullMessage, - state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "shardIndices" values for users still using it though + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + /* continue to send "shardIndices" values for users still using it though we have replaced it with shardIndex in the template due to alerts per index instead of all indices see https://github.com/elastic/kibana/issues/100136#issuecomment-865229431 */ - shardIndices: shardIndex, - shardIndex, - clusterName: cluster.clusterName, - action, - actionPlain: shortActionText, + shardIndices: shardIndex, + shardIndex, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/rules/license_expiration_rule.test.ts b/x-pack/plugins/monitoring/server/rules/license_expiration_rule.test.ts new file mode 100644 index 0000000000000..f1c193ce5f8ee --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/license_expiration_rule.test.ts @@ -0,0 +1,387 @@ +/* + * 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 { LicenseExpirationRule } from './license_expiration_rule'; +import { RULE_LICENSE_EXPIRATION } from '../../common/constants'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_licenses', () => ({ + fetchLicenses: jest.fn(), +})); +jest.mock('../lib/alerts/fetch_clusters', () => ({ + fetchClusters: jest.fn(), +})); + +jest.mock('../static_globals', () => ({ + Globals: { + app: { + getLogger: () => ({ debug: jest.fn() }), + config: { + ui: { + show_license_expiration: true, + ccs: { enabled: true }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('LicenseExpirationRule', () => { + it('should have defaults', () => { + const rule = new LicenseExpirationRule(); + expect(rule.ruleOptions.id).toBe(RULE_LICENSE_EXPIRATION); + expect(rule.ruleOptions.name).toBe('License expiration'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ + { name: 'expiredDate', description: 'The date when the license expires.' }, + { name: 'clusterName', description: 'The cluster to which the license belong.' }, + { + 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: '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 license = { + status: 'expired', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 59, + clusterUuid, + }; + + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [license]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should fire action', async () => { + jest.useFakeTimers().setSystemTime(new Date('2023-03-30T00:00:00.000Z')); + const alert = new LicenseExpirationRule(); + const type = alert.getRuleType(); + await type.executor({ + ...executorOptions, + params: alert.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: undefined, + itemLabel: undefined, + meta: { + clusterUuid: 'abc123', + expiryDateMS: 5097600000, + status: 'expired', + type: 'gold', + }, + nodeId: undefined, + nodeName: undefined, + ui: { + isFiring: true, + message: { + text: 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link', + tokens: [ + { + startToken: '#relative', + type: 'time', + isRelative: true, + isAbsolute: false, + timestamp: 5097600000, + }, + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 5097600000, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'license', + }, + ], + }, + severity: 'danger', + triggeredMS: 1680134400000, + lastCheckedMS: 0, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + action: '[Please update your license.](elasticsearch/nodes)', + actionPlain: 'Please update your license.', + internalFullMessage: + 'License expiration alert is firing for testCluster. Your license expires in 53 years. [Please update your license.](elasticsearch/nodes)', + internalShortMessage: + 'License expiration alert is firing for testCluster. Your license expires in 53 years. Please update your license.', + clusterName, + expiredDate: '53 years', + state: 'firing', + }, + payload: { + [ALERT_REASON]: + 'License expiration alert is firing for testCluster. Your license expires in 53 years. Please update your license.', + }, + }); + }); + + it('should not fire actions if the license is not expired', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 61, + clusterUuid, + }, + ]; + }); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); + }); + + it('should use danger severity for a license expiring soon', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 2, + clusterUuid, + }, + ]; + }); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: undefined, + itemLabel: undefined, + meta: { + clusterUuid: 'abc123', + expiryDateMS: 172800000, + status: 'active', + type: 'gold', + }, + nodeId: undefined, + nodeName: undefined, + ui: { + isFiring: true, + message: { + text: 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link', + tokens: [ + { + startToken: '#relative', + type: 'time', + isRelative: true, + isAbsolute: false, + timestamp: 172800000, + }, + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 172800000, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'license', + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + action: '[Please update your license.](elasticsearch/nodes)', + actionPlain: 'Please update your license.', + internalFullMessage: + 'License expiration alert is firing for testCluster. Your license expires in 2 days. [Please update your license.](elasticsearch/nodes)', + internalShortMessage: + 'License expiration alert is firing for testCluster. Your license expires in 2 days. Please update your license.', + clusterName, + expiredDate: '2 days', + state: 'firing', + }, + payload: { + [ALERT_REASON]: + 'License expiration alert is firing for testCluster. Your license expires in 2 days. Please update your license.', + }, + }); + }); + + it('should use warning severity for a license expiring in a bit', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 31, + clusterUuid, + }, + ]; + }); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid, clusterName }, + ccs: undefined, + itemLabel: undefined, + meta: { + clusterUuid: 'abc123', + expiryDateMS: 2678400000, + status: 'active', + type: 'gold', + }, + nodeId: undefined, + nodeName: undefined, + ui: { + isFiring: true, + message: { + text: 'The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link', + tokens: [ + { + startToken: '#relative', + type: 'time', + isRelative: true, + isAbsolute: false, + timestamp: 2678400000, + }, + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 2678400000, + }, + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'license', + }, + ], + }, + severity: 'warning', + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + action: '[Please update your license.](elasticsearch/nodes)', + actionPlain: 'Please update your license.', + internalFullMessage: + 'License expiration alert is firing for testCluster. Your license expires in a month. [Please update your license.](elasticsearch/nodes)', + internalShortMessage: + 'License expiration alert is firing for testCluster. Your license expires in a month. Please update your license.', + clusterName, + expiredDate: 'a month', + state: 'firing', + }, + payload: { + [ALERT_REASON]: + 'License expiration alert is firing for testCluster. Your license expires in a month. Please update your license.', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts b/x-pack/plugins/monitoring/server/rules/license_expiration_rule.ts similarity index 76% rename from x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts rename to x-pack/plugins/monitoring/server/rules/license_expiration_rule.ts index 6be4aa3edc1a8..b3ed188f33ee2 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/license_expiration_rule.ts @@ -7,8 +7,10 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { RuleExecutorOptions, Alert } from '@kbn/alerting-plugin/server'; -import { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { RuleExecutorOptions, RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -143,7 +145,13 @@ export class LicenseExpirationRule extends BaseRule { } protected async executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -161,34 +169,41 @@ export class LicenseExpirationRule extends BaseRule { }); const action = `[${actionText}](elasticsearch/nodes)`; const expiredDate = $duration.humanize(); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - expiredDate, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + actionText, + }, + } + ); + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + expiredDate, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts b/x-pack/plugins/monitoring/server/rules/logstash_version_mismatch_rule.test.ts similarity index 61% rename from x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts rename to x-pack/plugins/monitoring/server/rules/logstash_version_mismatch_rule.test.ts index aaa0260c1cee5..97cbbd2c619a6 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts +++ b/x-pack/plugins/monitoring/server/rules/logstash_version_mismatch_rule.test.ts @@ -9,7 +9,8 @@ import { LogstashVersionMismatchRule } from './logstash_version_mismatch_rule'; import { RULE_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; const RealDate = Date; @@ -80,24 +81,8 @@ describe('LogstashVersionMismatchRule', () => { }, ]; - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; beforeEach(() => { // @ts-ignore @@ -112,52 +97,63 @@ describe('LogstashVersionMismatchRule', () => { afterEach(() => { Date = RealDate; - replaceState.mockReset(); - scheduleActions.mockReset(); - getState.mockReset(); + jest.resetAllMocks(); }); - it('should fire actions', async () => { + it('should fire action', async () => { const rule = new LogstashVersionMismatchRule(); const type = rule.getRuleType(); await type.executor({ ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs, - itemLabel: undefined, - nodeId: undefined, - nodeName: undefined, - meta: { + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs, - clusterUuid, - versions: ['8.0.0', '7.2.1'], - }, - ui: { - isFiring: true, - message: { - text: 'Multiple versions of Logstash (8.0.0, 7.2.1) running in this cluster.', + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, + ui: { + isFiring: true, + message: { + text: 'Multiple versions of Logstash (8.0.0, 7.2.1) running in this cluster.', + }, + severity: 'warning', + triggeredMS: 1, + lastCheckedMS: 0, }, - severity: 'warning', - triggeredMS: 1, - lastCheckedMS: 0, }, - }, - ], + ], + }, }); - expect(scheduleActions).toHaveBeenCalledWith('default', { - action: `[View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, - actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: `Logstash version mismatch alert is firing for testCluster. Logstash is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, - internalShortMessage: - 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: ['8.0.0', '7.2.1'], - clusterName, - state: 'firing', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, + actionPlain: 'Verify you have the same version across all nodes.', + internalFullMessage: `Logstash version mismatch alert is firing for testCluster. Logstash is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, + internalShortMessage: + 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', + versionList: ['8.0.0', '7.2.1'], + clusterName, + state: 'firing', + }, + payload: { + [ALERT_REASON]: + 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', + }, }); }); @@ -177,8 +173,8 @@ describe('LogstashVersionMismatchRule', () => { ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).not.toHaveBeenCalledWith({}); - expect(scheduleActions).not.toHaveBeenCalled(); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts b/x-pack/plugins/monitoring/server/rules/logstash_version_mismatch_rule.ts similarity index 70% rename from x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts rename to x-pack/plugins/monitoring/server/rules/logstash_version_mismatch_rule.ts index a21e54c2b3c11..100e33d272844 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/logstash_version_mismatch_rule.ts @@ -7,8 +7,10 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -87,7 +89,13 @@ export class LogstashVersionMismatchRule extends BaseRule { } protected async executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -118,33 +126,40 @@ export class LogstashVersionMismatchRule extends BaseRule { state.ccs ); const action = `[${fullActionText}](${globalStateLink})`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions: versions.join(', '), - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', + { + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ); + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/rules/memory_usage_rule.test.ts b/x-pack/plugins/monitoring/server/rules/memory_usage_rule.test.ts new file mode 100644 index 0000000000000..c1a8836e293c1 --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/memory_usage_rule.test.ts @@ -0,0 +1,399 @@ +/* + * 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 { MemoryUsageRule } from './memory_usage_rule'; +import { RULE_MEMORY_USAGE } from '../../common/constants'; +import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +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 }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('MemoryUsageRule', () => { + it('should have defaults', () => { + const rule = new MemoryUsageRule(); + expect(rule.ruleOptions.id).toBe(RULE_MEMORY_USAGE); + expect(rule.ruleOptions.name).toBe('Memory Usage (JVM)'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); + expect(rule.ruleOptions.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 services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => { + return [stat]; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + it('should fire action', async () => { + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'myNodeId', + actionGroup: 'default', + state: { + 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(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'myNodeId', + context: { + 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', + }, + payload: { + [ALERT_REASON]: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`, + }, + }); + }); + + it('should not fire actions if under threshold', async () => { + (fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + memoryUsage: 1, + }, + ]; + }); + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchMemoryUsageNodeStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat, + ccs, + }, + ]; + }); + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'myNodeId', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: 'testCluster', + cluster: { clusterUuid, clusterName }, + memoryUsage, + itemLabel: undefined, + meta: { + ccs: 'testCluster', + 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(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'myNodeId', + context: { + 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', + }, + payload: { + [ALERT_REASON]: `Memory usage alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify memory usage level of node.`, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts b/x-pack/plugins/monitoring/server/rules/memory_usage_rule.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts rename to x-pack/plugins/monitoring/server/rules/memory_usage_rule.ts index fa94e7cb18b6e..9b7f1f72a47f1 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/memory_usage_rule.ts @@ -8,9 +8,11 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { AlertInstanceContext, RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common'; import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -157,7 +159,13 @@ export class MemoryUsageRule extends BaseRule { } protected executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -207,19 +215,25 @@ export class MemoryUsageRule extends BaseRule { } ); - 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 + services.alertsClient?.setAlertData({ + id: alertId, + context: { + 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, + nodes: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`, + count: 1, + node: `${firingNode.nodeName}:${firingNode.memoryUsage.toFixed(2)}`, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/rules/missing_monitoring_data_rule.test.ts b/x-pack/plugins/monitoring/server/rules/missing_monitoring_data_rule.test.ts new file mode 100644 index 0000000000000..8bf66a839d6dd --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/missing_monitoring_data_rule.test.ts @@ -0,0 +1,307 @@ +/* + * 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 { MissingMonitoringDataRule } from './missing_monitoring_data_rule'; +import { RULE_MISSING_MONITORING_DATA } from '../../common/constants'; +import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +const RealDate = Date; + +jest.mock('../lib/alerts/fetch_missing_monitoring_data', () => ({ + fetchMissingMonitoringData: 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 }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('MissingMonitoringDataRule', () => { + it('should have defaults', () => { + const rule = new MissingMonitoringDataRule(); + expect(rule.ruleOptions.id).toBe(RULE_MISSING_MONITORING_DATA); + expect(rule.ruleOptions.name).toBe('Missing monitoring data'); + expect(rule.ruleOptions.throttle).toBe('6h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ + { name: 'node', description: 'The node missing monitoring data.' }, + { + 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 gapDuration = 3000001; + const missingData = [ + { + nodeId, + nodeName, + clusterUuid, + gapDuration, + }, + ]; + + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { + return missingData; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + it('should fire action', async () => { + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'esNode1', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: undefined, + cluster: { clusterUuid, clusterName }, + nodeId, + nodeName, + gapDuration, + itemLabel: undefined, + meta: { + clusterUuid, + gapDuration, + limit: 86400000, + nodeId, + nodeName, + }, + ui: { + isFiring: true, + message: { + text: 'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute', + nextSteps: [ + { + text: '#start_linkView all Elasticsearch nodes#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/nodes', + }, + ], + }, + { + text: 'Verify monitoring settings on the node', + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'esNode1', + context: { + 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 the node is up and running, then double check the monitoring settings.', + clusterName, + count, + state: 'firing', + }, + payload: { + [ALERT_REASON]: `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.`, + }, + }); + }); + + it('should not fire actions if under threshold', async () => { + (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { + return [ + { + ...missingData[0], + gapDuration: 1, + }, + ]; + }); + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchMissingMonitoringData as jest.Mock).mockImplementation(() => { + return [ + { + ...missingData[0], + ccs, + }, + ]; + }); + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'esNode1', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: 'testCluster', + cluster: { clusterUuid, clusterName }, + nodeId, + nodeName, + gapDuration, + itemLabel: undefined, + meta: { + ccs: 'testCluster', + clusterUuid, + gapDuration, + limit: 86400000, + nodeId, + nodeName, + }, + ui: { + isFiring: true, + message: { + text: 'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute', + nextSteps: [ + { + text: '#start_linkView all Elasticsearch nodes#end_link', + tokens: [ + { + startToken: '#start_link', + endToken: '#end_link', + type: 'link', + url: 'elasticsearch/nodes', + }, + ], + }, + { + text: 'Verify monitoring settings on the node', + }, + ], + tokens: [ + { + startToken: '#absolute', + type: 'time', + isAbsolute: true, + isRelative: false, + timestamp: 1, + }, + ], + }, + severity: 'danger', + triggeredMS: 1, + lastCheckedMS: 0, + }, + }, + ], + }, + }); + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'esNode1', + context: { + 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 the node is up and running, then double check the monitoring settings.', + clusterName, + count, + state: 'firing', + }, + payload: { + [ALERT_REASON]: `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.`, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts b/x-pack/plugins/monitoring/server/rules/missing_monitoring_data_rule.ts similarity index 85% rename from x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts rename to x-pack/plugins/monitoring/server/rules/missing_monitoring_data_rule.ts index 0afd06708767a..a210d9ddf4209 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/missing_monitoring_data_rule.ts @@ -8,9 +8,16 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { RawAlertInstance, SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { + AlertInstanceContext, + AlertInstanceState, + RawAlertInstance, + SanitizedRule, +} from '@kbn/alerting-plugin/common'; import { parseDuration } from '@kbn/alerting-plugin/common/parse_duration'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -137,7 +144,13 @@ export class MissingMonitoringDataRule extends BaseRule { } protected executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster @@ -187,19 +200,25 @@ export class MissingMonitoringDataRule extends BaseRule { }, } ); - 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 + services.alertsClient?.setAlertData({ + id: alertId, + context: { + 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, + nodes: `node: ${firingNode.nodeName}`, + count: 1, + node: `node: ${firingNode.nodeName}`, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts b/x-pack/plugins/monitoring/server/rules/nodes_changed_rule.test.ts similarity index 50% rename from x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts rename to x-pack/plugins/monitoring/server/rules/nodes_changed_rule.test.ts index 02578102741da..3abd163343e9e 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts +++ b/x-pack/plugins/monitoring/server/rules/nodes_changed_rule.test.ts @@ -9,7 +9,8 @@ import { NodesChangedRule } from './nodes_changed_rule'; import { RULE_NODES_CHANGED } from '../../common/constants'; import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; -import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; const RealDate = Date; @@ -125,24 +126,8 @@ describe('NodesChangedAlert', () => { }, ]; - const replaceState = jest.fn(); - const scheduleActions = jest.fn(); - const getState = jest.fn(); - const executorOptions = { - services: { - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - alertFactory: { - create: jest.fn().mockImplementation(() => { - return { - replaceState, - scheduleActions, - getState, - }; - }), - }, - }, - state: {}, - }; + const services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; beforeEach(() => { // @ts-ignore @@ -154,12 +139,10 @@ describe('NodesChangedAlert', () => { afterEach(() => { Date = RealDate; - replaceState.mockReset(); - scheduleActions.mockReset(); - getState.mockReset(); + jest.resetAllMocks(); }); - it('should fire actions when nodes change', async () => { + it('should fire action when nodes change', async () => { (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { return nodesChanged; }); @@ -169,59 +152,72 @@ describe('NodesChangedAlert', () => { ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - cluster: { clusterUuid, clusterName }, - ccs, - itemLabel: undefined, - nodeId: undefined, - nodeName: undefined, - meta: { + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid, clusterName }, ccs, - clusterUuid, - recentNodes: [ - { - nodeUuid, - nodeEphemeralId: nodeEphemeralIdChanged, - nodeName, - }, - ], - priorNodes: [ - { - nodeUuid, - nodeEphemeralId, - nodeName, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + }, + ui: { + isFiring: true, + message: { + text: "Elasticsearch nodes 'test' restarted in this cluster.", }, - ], - }, - ui: { - isFiring: true, - message: { - text: "Elasticsearch nodes 'test' restarted in this cluster.", + severity: 'warning', + triggeredMS: 1, + lastCheckedMS: 0, }, - 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: removed: restarted:test. [View nodes](elasticsearch/nodes)', - internalShortMessage: - 'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.', - added: '', - removed: '', - restarted: 'test', - clusterName, - state: 'firing', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + 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: removed: restarted:test. [View nodes](elasticsearch/nodes)', + internalShortMessage: + 'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.', + added: '', + removed: '', + restarted: 'test', + clusterName, + state: 'firing', + }, + payload: { + [ALERT_REASON]: + 'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.', + }, }); }); - it('should fire actions when nodes added, changed, and removed', async () => { + it('should fire action when nodes added, changed, and removed', async () => { (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { return nodesAddedChangedRemoved; }); @@ -231,66 +227,79 @@ describe('NodesChangedAlert', () => { ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).toHaveBeenCalledWith({ - alertStates: [ - { - cluster: { clusterUuid, clusterName }, - ccs, - itemLabel: undefined, - nodeId: undefined, - nodeName: undefined, - meta: { + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'abc123', + actionGroup: 'default', + state: { + alertStates: [ + { + cluster: { clusterUuid, clusterName }, ccs, - clusterUuid, - recentNodes: [ - { - nodeUuid, - nodeEphemeralId: nodeEphemeralIdChanged, - nodeName, - }, - { - nodeUuid: 'newNodeId', - nodeEphemeralId: 'newNodeEmpheralId', - nodeName: 'newNodeName', - }, - ], - priorNodes: [ - { - nodeUuid, - nodeEphemeralId, - nodeName, - }, - { - nodeUuid: 'removedNodeId', - nodeEphemeralId: 'removedNodeEmpheralId', - nodeName: 'removedNodeName', + 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.", }, - ], - }, - 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, }, - 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', + expect(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'abc123', + context: { + 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', + }, + payload: { + [ALERT_REASON]: + 'Nodes changed alert is firing for testCluster. Verify that you added, removed, or restarted nodes.', + }, }); }); @@ -323,8 +332,8 @@ describe('NodesChangedAlert', () => { ...executorOptions, params: rule.ruleOptions.defaultParams, } as any); - expect(replaceState).not.toHaveBeenCalledWith({}); - expect(scheduleActions).not.toHaveBeenCalled(); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts b/x-pack/plugins/monitoring/server/rules/nodes_changed_rule.ts similarity index 80% rename from x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts rename to x-pack/plugins/monitoring/server/rules/nodes_changed_rule.ts index 6c20fe6326630..b433be6ac1dd9 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts +++ b/x-pack/plugins/monitoring/server/rules/nodes_changed_rule.ts @@ -7,8 +7,10 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { AlertInstanceContext, SanitizedRule } from '@kbn/alerting-plugin/common'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -174,7 +176,13 @@ export class NodesChangedRule extends BaseRule { } protected async executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: AlertInstanceState, item: AlertData | null, cluster: AlertCluster @@ -198,37 +206,44 @@ export class NodesChangedRule extends BaseRule { const added = states.added.map((node) => node.nodeName).join(','); const removed = states.removed.map((node) => node.nodeName).join(','); const restarted = states.restarted.map((node) => node.nodeName).join(','); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, - values: { - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - actionPlain: shortActionText, + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ); + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, + values: { + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/rules/rules_factory.test.ts similarity index 84% rename from x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts rename to x-pack/plugins/monitoring/server/rules/rules_factory.test.ts index c86a5264b204b..15fbc332cbb2f 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts +++ b/x-pack/plugins/monitoring/server/rules/rules_factory.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AlertsFactory } from './alerts_factory'; +import { RulesFactory } from './rules_factory'; import { RULE_CPU_USAGE } from '../../common/constants'; jest.mock('../static_globals', () => ({ @@ -16,7 +16,7 @@ jest.mock('../static_globals', () => ({ }, })); -describe('AlertsFactory', () => { +describe('RulesFactory', () => { const rulesClient = { find: jest.fn(), }; @@ -39,7 +39,7 @@ describe('AlertsFactory', () => { ], }; }); - const alerts = await AlertsFactory.getByType(RULE_CPU_USAGE, rulesClient as any); + const alerts = await RulesFactory.getByType(RULE_CPU_USAGE, rulesClient as any); expect(alerts).not.toBeNull(); expect(alerts.length).toBe(2); expect(alerts[0].getId()).toBe(1); @@ -54,7 +54,7 @@ describe('AlertsFactory', () => { total: 0, }; }); - await AlertsFactory.getByType(RULE_CPU_USAGE, rulesClient as any); + await RulesFactory.getByType(RULE_CPU_USAGE, rulesClient as any); expect(filter).toBe(`alert.attributes.alertTypeId:${RULE_CPU_USAGE}`); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/rules/rules_factory.ts similarity index 81% rename from x-pack/plugins/monitoring/server/alerts/alerts_factory.ts rename to x-pack/plugins/monitoring/server/rules/rules_factory.ts index 9cebe881936a3..72aea1b57ea6e 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts +++ b/x-pack/plugins/monitoring/server/rules/rules_factory.ts @@ -40,7 +40,7 @@ import { RULE_CCR_READ_EXCEPTIONS, RULE_LARGE_SHARD_SIZE, } from '../../common/constants'; -import { CommonAlertParams } from '../../common/types/alerts'; +import { CommonAlertParams as CommonRuleParams } from '../../common/types/alerts'; const BY_TYPE = { [RULE_CLUSTER_HEALTH]: ClusterHealthRule, @@ -59,28 +59,28 @@ const BY_TYPE = { [RULE_LARGE_SHARD_SIZE]: LargeShardSizeRule, }; -export class AlertsFactory { +export class RulesFactory { public static async getByType( type: string, - alertsClient: RulesClient | undefined + rulesClient: RulesClient | undefined ): Promise { - const alertCls = BY_TYPE[type]; - if (!alertCls || !alertsClient) { + const ruleCls = BY_TYPE[type]; + if (!ruleCls || !rulesClient) { return []; } - const alertClientAlerts = await alertsClient.find({ + const rulesClientRules = await rulesClient.find({ options: { filter: `alert.attributes.alertTypeId:${type}`, }, }); - if (!alertClientAlerts.total || !alertClientAlerts.data?.length) { + if (!rulesClientRules.total || !rulesClientRules.data?.length) { return []; } - return alertClientAlerts.data.map((alert) => new alertCls(alert as Rule) as BaseRule); + return rulesClientRules.data.map((rule) => new ruleCls(rule as Rule) as BaseRule); } public static getAll() { - return Object.values(BY_TYPE).map((alert) => new alert()); + return Object.values(BY_TYPE).map((rule) => new rule()); } } diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts b/x-pack/plugins/monitoring/server/rules/thread_pool_rejections_rule_base.ts similarity index 88% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts rename to x-pack/plugins/monitoring/server/rules/thread_pool_rejections_rule_base.ts index e5b4b7d691a52..ed453b0e208f7 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts +++ b/x-pack/plugins/monitoring/server/rules/thread_pool_rejections_rule_base.ts @@ -7,8 +7,15 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from '@kbn/core/server'; -import { Alert } from '@kbn/alerting-plugin/server'; -import { Rule, RawAlertInstance } from '@kbn/alerting-plugin/common'; +import type { DefaultAlert } from '@kbn/alerts-as-data-utils'; +import { + Rule, + RawAlertInstance, + AlertInstanceState, + AlertInstanceContext, +} from '@kbn/alerting-plugin/common'; +import { RuleExecutorServices } from '@kbn/alerting-plugin/server'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; import { BaseRule } from './base_rule'; import { AlertData, @@ -176,7 +183,13 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { }; } protected executeActions( - instance: Alert, + services: RuleExecutorServices< + AlertInstanceState, + AlertInstanceContext, + 'default', + DefaultAlert + >, + alertId: string, { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster @@ -243,19 +256,25 @@ export class ThreadPoolRejectionsRuleBase extends BaseRule { } ); - instance.scheduleActions('default', { - internalShortMessage, - internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, - threadPoolType: type, - state: AlertingDefaults.ALERT_STATE.firing, - /* continue to send "count" value for users before https://github.com/elastic/kibana/pull/102544 + services.alertsClient?.setAlertData({ + id: alertId, + context: { + internalShortMessage, + internalFullMessage: Globals.app.isCloud ? internalShortMessage : internalFullMessage, + threadPoolType: type, + state: AlertingDefaults.ALERT_STATE.firing, + /* 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, + count: 1, + node: nodeName, + clusterName, + action, + actionPlain: shortActionText, + }, + payload: { + [ALERT_REASON]: internalShortMessage, + }, }); } } diff --git a/x-pack/plugins/monitoring/server/rules/thread_pool_search_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/rules/thread_pool_search_rejections_rule.test.ts new file mode 100644 index 0000000000000..c6602f536a83c --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/thread_pool_search_rejections_rule.test.ts @@ -0,0 +1,406 @@ +/* + * 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 { ThreadPoolSearchRejectionsRule } from './thread_pool_search_rejections_rule'; +import { RULE_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 { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +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 }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('ThreadpoolSearchRejectionsRule', () => { + it('should have defaults', () => { + const rule = new ThreadPoolSearchRejectionsRule(); + expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_SEARCH_REJECTIONS); + expect(rule.ruleOptions.name).toBe('Thread pool search rejections'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); + expect(rule.ruleOptions.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 services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return stat; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + it('should fire action', async () => { + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'esNode1', + actionGroup: 'default', + state: { + 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(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'esNode1', + context: { + 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', + }, + payload: { + [ALERT_REASON]: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`, + }, + }); + }); + it('should not fire actions if under threshold', async () => { + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat[0], + rejectionCount: 1, + }, + ]; + }); + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat[0], + ccs, + }, + ]; + }); + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'esNode1', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: 'testCluster', + cluster: { clusterUuid, clusterName }, + nodeId, + nodeName, + itemLabel: undefined, + meta: { + rejectionCount, + clusterUuid, + type: threadPoolType, + nodeId, + nodeName, + ccs: 'testCluster', + }, + 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(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'esNode1', + context: { + 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, + }, + payload: { + [ALERT_REASON]: `Thread pool search rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts b/x-pack/plugins/monitoring/server/rules/thread_pool_search_rejections_rule.ts similarity index 100% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts rename to x-pack/plugins/monitoring/server/rules/thread_pool_search_rejections_rule.ts diff --git a/x-pack/plugins/monitoring/server/rules/thread_pool_write_rejections_rule.test.ts b/x-pack/plugins/monitoring/server/rules/thread_pool_write_rejections_rule.test.ts new file mode 100644 index 0000000000000..163dc2b1a677b --- /dev/null +++ b/x-pack/plugins/monitoring/server/rules/thread_pool_write_rejections_rule.test.ts @@ -0,0 +1,406 @@ +/* + * 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 { ThreadPoolWriteRejectionsRule } from './thread_pool_write_rejections_rule'; +import { RULE_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 { alertsMock } from '@kbn/alerting-plugin/server/mocks'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; + +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 }, + container: { elasticsearch: { enabled: false } }, + }, + }, + }, + }, +})); + +describe('ThreadpoolWriteRejectionsAlert', () => { + it('should have defaults', () => { + const rule = new ThreadPoolWriteRejectionsRule(); + expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_WRITE_REJECTIONS); + expect(rule.ruleOptions.name).toBe(`Thread pool write rejections`); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); + expect(rule.ruleOptions.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 services = alertsMock.createRuleExecutorServices(); + const executorOptions = { services, state: {} }; + + beforeEach(() => { + // @ts-ignore + Date = FakeDate; + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return stat; + }); + (fetchClusters as jest.Mock).mockImplementation(() => { + return [{ clusterUuid, clusterName }]; + }); + }); + + afterEach(() => { + Date = RealDate; + jest.resetAllMocks(); + }); + + it('should fire action', async () => { + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'esNode1', + actionGroup: 'default', + state: { + 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(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'esNode1', + context: { + 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', + }, + payload: { + [ALERT_REASON]: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`, + }, + }); + }); + it('should not fire actions if under threshold', async () => { + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat[0], + rejectionCount: 1, + }, + ]; + }); + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + expect(services.alertsClient.report).not.toHaveBeenCalled(); + expect(services.alertsClient.setAlertData).not.toHaveBeenCalled(); + }); + + it('should handle ccs', async () => { + const ccs = 'testCluster'; + (fetchThreadPoolRejectionStats as jest.Mock).mockImplementation(() => { + return [ + { + ...stat[0], + ccs, + }, + ]; + }); + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); + await type.executor({ + ...executorOptions, + params: rule.ruleOptions.defaultParams, + } as any); + const count = 1; + expect(services.alertsClient.report).toHaveBeenCalledTimes(1); + expect(services.alertsClient.setAlertData).toHaveBeenCalledTimes(1); + expect(services.alertsClient.report).toHaveBeenCalledWith({ + id: 'esNode1', + actionGroup: 'default', + state: { + alertStates: [ + { + ccs: 'testCluster', + cluster: { clusterUuid, clusterName }, + nodeId, + nodeName, + itemLabel: undefined, + meta: { + rejectionCount, + clusterUuid, + type: threadPoolType, + nodeId, + nodeName, + ccs: 'testCluster', + }, + 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(services.alertsClient.setAlertData).toHaveBeenCalledWith({ + id: 'esNode1', + context: { + 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, + }, + payload: { + [ALERT_REASON]: `Thread pool ${threadPoolType} rejections alert is firing for node ${nodeName} in cluster: ${clusterName}. Verify thread pool ${threadPoolType} rejections for the affected node.`, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts b/x-pack/plugins/monitoring/server/rules/thread_pool_write_rejections_rule.ts similarity index 100% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts rename to x-pack/plugins/monitoring/server/rules/thread_pool_write_rejections_rule.ts diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index 00ca962568141..112bf5f695b17 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -42,6 +42,8 @@ "@kbn/observability-shared-plugin", "@kbn/shared-ux-link-redirect-app", "@kbn/logs-shared-plugin", + "@kbn/alerts-as-data-utils", + "@kbn/rule-data-utils", ], "exclude": [ "target/**/*",