From 1192761a233bc2b3556d0acf20d9e1c3aa771673 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Thu, 31 Oct 2024 12:30:47 -0400 Subject: [PATCH] [Response Ops][Alerting] Removing lifecycle executor from rule registry (#192576) ## Summary All lifecycle rule types have been migrated to use the alerting framework alerts client so the lifecycle executor in the rule registry can be removed since it is no longer in use. Co-authored-by: Elastic Machine --- ...nventory_metric_threshold_executor.test.ts | 19 +- .../server/services/rules/rules_service.ts | 8 +- .../infra/server/services/rules/types.ts | 9 +- x-pack/plugins/rule_registry/server/index.ts | 7 - x-pack/plugins/rule_registry/server/mocks.ts | 2 - x-pack/plugins/rule_registry/server/plugin.ts | 3 - .../utils/create_lifecycle_executor.test.ts | 2408 ----------------- .../server/utils/create_lifecycle_executor.ts | 479 ---- .../create_lifecycle_rule_executor_mock.ts | 38 - .../utils/create_lifecycle_rule_type.test.ts | 512 ---- .../create_lifecycle_rule_type_factory.ts | 45 - .../get_updated_flapping_history.test.ts | 207 -- .../utils/get_updated_flapping_history.ts | 64 - .../utils/lifecycle_alert_services.mock.ts | 41 - x-pack/plugins/rule_registry/tsconfig.json | 1 - .../plugins/alerts/server/rule_types.ts | 47 +- .../common/plugins/alerts/tsconfig.json | 1 - .../group1/tests/alerting/bulk_untrack.ts | 1 + .../tests/alerting/bulk_untrack_by_query.ts | 1 + .../group4/tests/alerting/alerts.ts | 60 +- .../tests/alerting/group1/disable.ts | 1 + .../alerts_as_data_alert_delay.ts | 232 +- .../tests/alerting/group4/bulk_disable.ts | 1 + .../spaces_only/tests/trial/index.ts | 1 - .../tests/trial/lifecycle_executor.ts | 275 -- 25 files changed, 74 insertions(+), 4389 deletions(-) delete mode 100644 x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts delete mode 100644 x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts delete mode 100644 x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts delete mode 100644 x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts delete mode 100644 x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts delete mode 100644 x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts delete mode 100644 x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts delete mode 100644 x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts delete mode 100644 x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts index c54b29d52714f..a29308774440c 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts @@ -6,14 +6,7 @@ */ import rison from '@kbn/rison'; -import { - AlertInstanceContext as AlertContext, - AlertInstanceState as AlertState, -} from '@kbn/alerting-plugin/server'; import { RuleExecutorServicesMock, alertsMock } from '@kbn/alerting-plugin/server/mocks'; -import { LifecycleAlertServices } from '@kbn/rule-registry-plugin/server'; -import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks'; -import { createLifecycleRuleExecutorMock } from '@kbn/rule-registry-plugin/server/utils/create_lifecycle_rule_executor_mock'; import { COMPARATORS } from '@kbn/alerting-comparators'; import { Aggregators, InventoryMetricConditions } from '../../../../common/alerting/metrics'; import type { LogMeta, Logger } from '@kbn/logging'; @@ -150,9 +143,7 @@ const mockLibs = { infraPluginMock.createStartContract(), ], configuration: createMockStaticConfiguration({}), - metricsRules: { - createLifecycleRuleExecutor: createLifecycleRuleExecutorMock, - }, + metricsRules: {}, basePath: { publicBaseUrl: 'http://localhost:5601', prepend: (path: string) => path, @@ -165,14 +156,10 @@ const mockLibs = { logger, } as unknown as InfraBackendLibs; const alerts = new Map(); -let services: RuleExecutorServicesMock & LifecycleAlertServices; +let services: RuleExecutorServicesMock; const setup = () => { - const alertsServices = alertsMock.createRuleExecutorServices(); - services = { - ...alertsServices, - ...ruleRegistryMocks.createLifecycleAlertServices(alertsServices), - }; + services = alertsMock.createRuleExecutorServices(); services.alertsClient.report.mockImplementation((params: any) => { alerts.set(params.id, { actionGroup: params.actionGroup, context: [], payload: [] }); diff --git a/x-pack/plugins/observability_solution/infra/server/services/rules/rules_service.ts b/x-pack/plugins/observability_solution/infra/server/services/rules/rules_service.ts index 85d3d8548fbe6..99e7c57d857b5 100644 --- a/x-pack/plugins/observability_solution/infra/server/services/rules/rules_service.ts +++ b/x-pack/plugins/observability_solution/infra/server/services/rules/rules_service.ts @@ -6,7 +6,6 @@ */ import { CoreSetup, Logger } from '@kbn/core/server'; -import { createLifecycleExecutor } from '@kbn/rule-registry-plugin/server'; import { InfraFeatureId } from '../../../common/constants'; import { createRuleDataClient } from './rule_data_client'; import { @@ -36,12 +35,7 @@ export class RulesService { ruleDataService: setupDeps.ruleRegistry.ruleDataService, }); - const createLifecycleRuleExecutor = createLifecycleExecutor(this.logger, ruleDataClient); - - return { - createLifecycleRuleExecutor, - ruleDataClient, - }; + return { ruleDataClient }; } public start(_startDeps: RulesServiceStartDeps): RulesServiceStart { diff --git a/x-pack/plugins/observability_solution/infra/server/services/rules/types.ts b/x-pack/plugins/observability_solution/infra/server/services/rules/types.ts index fa14089de2ba5..68ae0bd95b410 100644 --- a/x-pack/plugins/observability_solution/infra/server/services/rules/types.ts +++ b/x-pack/plugins/observability_solution/infra/server/services/rules/types.ts @@ -6,13 +6,7 @@ */ import { PluginSetupContract as AlertingPluginSetup } from '@kbn/alerting-plugin/server'; -import { - createLifecycleExecutor, - IRuleDataClient, - RuleRegistryPluginSetupContract, -} from '@kbn/rule-registry-plugin/server'; - -type LifecycleRuleExecutorCreator = ReturnType; +import { IRuleDataClient, RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server'; export interface RulesServiceSetupDeps { alerting: AlertingPluginSetup; ruleRegistry: RuleRegistryPluginSetupContract; @@ -22,7 +16,6 @@ export interface RulesServiceSetupDeps { export interface RulesServiceStartDeps {} export interface RulesServiceSetup { - createLifecycleRuleExecutor: LifecycleRuleExecutorCreator; ruleDataClient: IRuleDataClient; } diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 826d0d6f23bab..de0685b8c9617 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -25,13 +25,6 @@ export * from './rule_data_plugin_service'; export * from './rule_data_client'; export * from './alert_data_client/audit_events'; -export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; -export type { - LifecycleRuleExecutor, - LifecycleAlertService, - LifecycleAlertServices, -} from './utils/create_lifecycle_executor'; -export { createLifecycleExecutor } from './utils/create_lifecycle_executor'; export { createPersistenceRuleTypeWrapper } from './utils/create_persistence_rule_type_wrapper'; export * from './utils/persistence_types'; export type { AlertsClient } from './alert_data_client/alerts_client'; diff --git a/x-pack/plugins/rule_registry/server/mocks.ts b/x-pack/plugins/rule_registry/server/mocks.ts index 7ab1391ca1dec..ef5ae00ca0c56 100644 --- a/x-pack/plugins/rule_registry/server/mocks.ts +++ b/x-pack/plugins/rule_registry/server/mocks.ts @@ -11,10 +11,8 @@ import { ruleDataServiceMock, RuleDataServiceMock, } from './rule_data_plugin_service/rule_data_plugin_service.mock'; -import { createLifecycleAlertServicesMock } from './utils/lifecycle_alert_services.mock'; export const ruleRegistryMocks = { - createLifecycleAlertServices: createLifecycleAlertServicesMock, createRuleDataService: ruleDataServiceMock.create, createRuleDataClient: createRuleDataClientMock, createAlertsClientMock: alertsClientMock, diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 7f6b6e0bf6002..60ee2256ae377 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -29,7 +29,6 @@ import type { PluginSetup as DataPluginSetup, } from '@kbn/data-plugin/server'; -import { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; import type { RuleRegistryPluginConfig } from './config'; import { type IRuleDataService, RuleDataService, Dataset } from './rule_data_plugin_service'; import { AlertsClientFactory } from './alert_data_client/alerts_client_factory'; @@ -52,7 +51,6 @@ export interface RuleRegistryPluginStartDependencies { export interface RuleRegistryPluginSetupContract { ruleDataService: IRuleDataService; - createLifecycleRuleTypeFactory: typeof createLifecycleRuleTypeFactory; dataset: typeof Dataset; } @@ -153,7 +151,6 @@ export class RuleRegistryPlugin return { ruleDataService: this.ruleDataService, - createLifecycleRuleTypeFactory, dataset: Dataset, }; } diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts deleted file mode 100644 index b895c49c14a5f..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ /dev/null @@ -1,2408 +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 { loggerMock } from '@kbn/logging-mocks'; -import { pick } from 'lodash'; -import { - ALERT_INSTANCE_ID, - ALERT_MAINTENANCE_WINDOW_IDS, - ALERT_RULE_CATEGORY, - ALERT_RULE_CONSUMER, - ALERT_RULE_NAME, - ALERT_RULE_PRODUCER, - ALERT_RULE_TYPE_ID, - ALERT_RULE_UUID, - ALERT_STATUS, - ALERT_STATUS_ACTIVE, - ALERT_STATUS_RECOVERED, - ALERT_WORKFLOW_STATUS, - ALERT_UUID, - EVENT_ACTION, - EVENT_KIND, - SPACE_IDS, - ALERT_FLAPPING, - TAGS, - ALERT_CONSECUTIVE_MATCHES, -} from '../../common/technical_rule_data_field_names'; -import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; -import { createLifecycleExecutor } from './create_lifecycle_executor'; -import { createDefaultAlertExecutorOptions } from './rule_executor.test_helpers'; - -describe('createLifecycleExecutor', () => { - it('wraps and unwraps the original executor state', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - // @ts-ignore 4.3.5 upgrade - Expression produces a union type that is too complex to represent.ts(2590) - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async (options) => { - expect(options.state).toEqual(initialRuleState); - - const nextRuleState: TestRuleState = { - aRuleStateKey: 'NEXT_RULE_STATE_VALUE', - }; - - return { state: nextRuleState }; - }); - - const newExecutorResult = await executor( - createDefaultAlertExecutorOptions({ - params: {}, - state: { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }, - logger, - }) - ); - - expect(newExecutorResult.state).toEqual({ - wrapped: { - aRuleStateKey: 'NEXT_RULE_STATE_VALUE', - }, - trackedAlerts: {}, - trackedAlertsRecovered: {}, - }); - }); - - it('writes initial documents for newly firing alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: { [TAGS]: ['source-tag1', 'source-tag2'] }, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: { [TAGS]: ['source-tag3', 'source-tag4'] }, - }); - - return { state }; - }); - - await executor( - createDefaultAlertExecutorOptions({ - params: {}, - state: { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }, - logger, - }) - ); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: [ - // alert documents - { create: { _id: expect.any(String) } }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [TAGS]: ['source-tag1', 'source-tag2', 'rule-tag1', 'rule-tag2'], - }), - { create: { _id: expect.any(String) } }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [TAGS]: ['source-tag3', 'source-tag4', 'rule-tag1', 'rule-tag2'], - }), - ], - }) - ); - expect((await ruleDataClientMock.getWriter()).bulk).not.toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // evaluation documents - { create: {} }, - expect.objectContaining({ - [EVENT_KIND]: 'event', - }), - ]), - }) - ); - }); - - it('updates existing documents for repeatedly firing alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'closed', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 1, - _primary_term: 3, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: [ - // alert document - { - index: { - _id: 'TEST_ALERT_0_UUID', - _index: '.alerts-index-name', - if_primary_term: 2, - if_seq_no: 4, - require_alias: false, - }, - }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_WORKFLOW_STATUS]: 'closed', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, - - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - }), - { - index: { - _id: 'TEST_ALERT_1_UUID', - _index: '.alerts-index-name', - if_primary_term: 3, - if_seq_no: 1, - require_alias: false, - }, - }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - }), - ], - }) - ); - expect((await ruleDataClientMock.getWriter()).bulk).not.toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // evaluation documents - { index: {} }, - expect.objectContaining({ - [EVENT_KIND]: 'event', - }), - ]), - }) - ); - }); - - it('logs warning if existing documents are in unexpected index', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'closed', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - }, - _index: 'partial-.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 1, - _primary_term: 3, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: [ - // alert document - { - index: { - _id: 'TEST_ALERT_1_UUID', - _index: '.alerts-index-name', - if_primary_term: 3, - if_seq_no: 1, - require_alias: false, - }, - }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - }), - ], - }) - ); - expect((await ruleDataClientMock.getWriter()).bulk).not.toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // evaluation documents - { index: {} }, - expect.objectContaining({ - [EVENT_KIND]: 'event', - }), - ]), - }) - ); - expect(logger.warn).toHaveBeenCalledWith( - `Could not update alert TEST_ALERT_0 in partial-.alerts-index-name. Partial and restored alert indices are not supported.` - ); - }); - - it('updates existing documents for recovered alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - [TAGS]: ['source-tag1', 'source-tag2'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - [TAGS]: ['source-tag3', 'source-tag4'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - // TEST_ALERT_0 has recovered - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // alert document - { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, - [TAGS]: ['source-tag1', 'source-tag2', 'rule-tag1', 'rule-tag2'], - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [TAGS]: ['source-tag3', 'source-tag4', 'rule-tag1', 'rule-tag2'], - }), - ]), - }) - ); - expect((await ruleDataClientMock.getWriter()).bulk).not.toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // evaluation documents - { index: {} }, - expect.objectContaining({ - [EVENT_KIND]: 'event', - }), - ]), - }) - ); - }); - - it('does not write alert documents when rule execution is cancelled and feature flags indicate to skip', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async (options) => { - expect(options.state).toEqual(initialRuleState); - - const nextRuleState: TestRuleState = { - aRuleStateKey: 'NEXT_RULE_STATE_VALUE', - }; - - return { state: nextRuleState }; - }); - - await executor( - createDefaultAlertExecutorOptions({ - params: {}, - state: { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }, - shouldWriteAlerts: false, - logger, - }) - ); - - expect((await ruleDataClientMock.getWriter()).bulk).not.toHaveBeenCalled(); - }); - - it('throws error when writer initialization fails', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getWriter = jest - .fn() - .mockRejectedValueOnce(new Error('error initializing!')); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async (options) => { - const nextRuleState: TestRuleState = { - aRuleStateKey: 'NEXT_RULE_STATE_VALUE', - }; - - return { state: nextRuleState }; - }); - - await expect(() => - executor( - createDefaultAlertExecutorOptions({ - params: {}, - state: { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }, - shouldWriteAlerts: false, - logger, - }) - ) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"error initializing!"`); - }); - - describe('updating flappingHistory', () => { - it('sets flapping state to true on a new alert', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - const { - state: { trackedAlerts, trackedAlertsRecovered }, - } = await executor( - createDefaultAlertExecutorOptions({ - params: {}, - state: { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }, - logger, - }) - ); - - const alerts = pick(trackedAlerts, [ - 'TEST_ALERT_0.flappingHistory', - 'TEST_ALERT_1.flappingHistory', - ]); - expect(alerts).toMatchInlineSnapshot(` - Object { - "TEST_ALERT_0": Object { - "flappingHistory": Array [ - true, - ], - }, - "TEST_ALERT_1": Object { - "flappingHistory": Array [ - true, - ], - }, - } - `); - expect(trackedAlertsRecovered).toMatchInlineSnapshot(`Object {}`); - }); - - it('sets flapping state to false on an alert that is still active', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'closed', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - const { - state: { trackedAlerts, trackedAlertsRecovered }, - } = await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - const alerts = pick(trackedAlerts, [ - 'TEST_ALERT_0.flappingHistory', - 'TEST_ALERT_1.flappingHistory', - ]); - expect(alerts).toMatchInlineSnapshot(` - Object { - "TEST_ALERT_0": Object { - "flappingHistory": Array [ - false, - ], - }, - "TEST_ALERT_1": Object { - "flappingHistory": Array [ - false, - ], - }, - } - `); - expect(trackedAlertsRecovered).toMatchInlineSnapshot(`Object {}`); - }); - - it('sets flapping state to true on an alert that is active and previously recovered', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'closed', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - const { - state: { trackedAlerts, trackedAlertsRecovered }, - } = await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlertsRecovered: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlerts: {}, - }, - logger, - }) - ); - - const alerts = pick(trackedAlerts, [ - 'TEST_ALERT_0.flappingHistory', - 'TEST_ALERT_1.flappingHistory', - ]); - expect(alerts).toMatchInlineSnapshot(` - Object { - "TEST_ALERT_0": Object { - "flappingHistory": Array [ - true, - ], - }, - "TEST_ALERT_1": Object { - "flappingHistory": Array [ - true, - ], - }, - } - `); - expect(trackedAlertsRecovered).toMatchInlineSnapshot(`Object {}`); - }); - - it('sets flapping state to true on an alert that is recovered and previously active', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - // TEST_ALERT_0 has recovered - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - const { - state: { trackedAlerts, trackedAlertsRecovered }, - } = await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - const recovered = pick(trackedAlertsRecovered, ['TEST_ALERT_0.flappingHistory']); - expect(recovered).toMatchInlineSnapshot(` - Object { - "TEST_ALERT_0": Object { - "flappingHistory": Array [ - true, - ], - }, - } - `); - const active = pick(trackedAlerts, ['TEST_ALERT_1.flappingHistory']); - expect(active).toMatchInlineSnapshot(` - Object { - "TEST_ALERT_1": Object { - "flappingHistory": Array [ - false, - ], - }, - } - `); - }); - - it('sets flapping state to false on an alert that is still recovered', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - // TEST_ALERT_0 has recovered - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - const { - state: { trackedAlerts, trackedAlertsRecovered }, - } = await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - }, - logger, - }) - ); - - const recovered = pick(trackedAlertsRecovered, ['TEST_ALERT_0.flappingHistory']); - expect(recovered).toMatchInlineSnapshot(`Object {}`); - const active = pick(trackedAlerts, ['TEST_ALERT_1.flappingHistory']); - expect(active).toMatchInlineSnapshot(` - Object { - "TEST_ALERT_1": Object { - "flappingHistory": Array [ - false, - ], - }, - } - `); - }); - }); - - describe('set maintenance window ids on the document', () => { - const maintenanceWindowIds = ['test-id-1', 'test-id-2']; - - it('updates documents with maintenance window ids for newly firing alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: { [TAGS]: ['source-tag1', 'source-tag2'] }, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: { [TAGS]: ['source-tag3', 'source-tag4'] }, - }); - - return { state }; - }); - - await executor( - createDefaultAlertExecutorOptions({ - params: {}, - state: { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }, - logger, - }) - ); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: [ - // alert documents - { create: { _id: expect.any(String) } }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [TAGS]: ['source-tag1', 'source-tag2', 'rule-tag1', 'rule-tag2'], - [ALERT_MAINTENANCE_WINDOW_IDS]: maintenanceWindowIds, - }), - { create: { _id: expect.any(String) } }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'open', - [EVENT_KIND]: 'signal', - [TAGS]: ['source-tag3', 'source-tag4', 'rule-tag1', 'rule-tag2'], - [ALERT_MAINTENANCE_WINDOW_IDS]: maintenanceWindowIds, - }), - ], - }) - ); - expect((await ruleDataClientMock.getWriter()).bulk).not.toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // evaluation documents - { index: {} }, - expect.objectContaining({ - [EVENT_KIND]: 'event', - }), - ]), - }) - ); - }); - - it('does not update documents with maintenance window ids for repeatedly firing alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'closed', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: [ - // alert document - { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_WORKFLOW_STATUS]: 'closed', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - }), - ], - }) - ); - expect((await ruleDataClientMock.getWriter()).bulk).not.toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // evaluation documents - { index: {} }, - expect.objectContaining({ - [EVENT_KIND]: 'event', - }), - ]), - }) - ); - }); - - it('does not update documents with maintenance window ids for recovered alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc - [TAGS]: ['source-tag1', 'source-tag2'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc - [TAGS]: ['source-tag3', 'source-tag4'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - // TEST_ALERT_0 has recovered - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - - return { state }; - }); - - await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // alert document - { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, - [TAGS]: ['source-tag1', 'source-tag2', 'rule-tag1', 'rule-tag2'], - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [TAGS]: ['source-tag3', 'source-tag4', 'rule-tag1', 'rule-tag2'], - }), - ]), - }) - ); - expect((await ruleDataClientMock.getWriter()).bulk).not.toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // evaluation documents - { index: {} }, - expect.objectContaining({ - [EVENT_KIND]: 'event', - }), - ]), - }) - ); - }); - }); - - describe('set flapping on the document', () => { - const flapping = new Array(16).fill(false).concat([true, true, true, true]); - const notFlapping = new Array(20).fill(false); - - it('updates documents with flapping for active alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'closed', - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', - [ALERT_UUID]: 'ALERT_2_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', - [ALERT_UUID]: 'ALERT_3_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_2', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_3', - fields: {}, - }); - - return { state }; - }); - - const serializedAlerts = await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: flapping, - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [false, false], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_2: { - alertId: 'TEST_ALERT_2', - alertUuid: 'TEST_ALERT_2_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: flapping, - flapping: true, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_3: { - alertId: 'TEST_ALERT_3', - alertUuid: 'TEST_ALERT_3_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [false, false], - flapping: true, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect(serializedAlerts.state.trackedAlerts).toEqual({ - TEST_ALERT_0: { - activeCount: 1, - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - flapping: true, - flappingHistory: flapping.slice(1).concat([false]), - pendingRecoveredCount: 0, - started: '2020-01-01T12:00:00.000Z', - }, - TEST_ALERT_1: { - activeCount: 1, - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - flapping: false, - flappingHistory: [false, false, false], - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - TEST_ALERT_2: { - activeCount: 1, - alertId: 'TEST_ALERT_2', - alertUuid: 'TEST_ALERT_2_UUID', - flapping: true, - flappingHistory: flapping.slice(1).concat([false]), - pendingRecoveredCount: 0, - started: '2020-01-01T12:00:00.000Z', - }, - TEST_ALERT_3: { - activeCount: 1, - alertId: 'TEST_ALERT_3', - alertUuid: 'TEST_ALERT_3_UUID', - flapping: true, - flappingHistory: [false, false, false], - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - }); - - expect(serializedAlerts.state.trackedAlertsRecovered).toEqual({}); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: [ - // alert document - { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_WORKFLOW_STATUS]: 'closed', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_FLAPPING]: false, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_FLAPPING]: false, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_FLAPPING]: true, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_FLAPPING]: true, - }), - ], - }) - ); - }); - - it('updates existing documents for recovered alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', - [ALERT_UUID]: 'ALERT_2_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', - [ALERT_UUID]: 'ALERT_3_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - return { state }; - }); - - const serializedAlerts = await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [true, true, true, true], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: notFlapping, - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_2: { - alertId: 'TEST_ALERT_2', - alertUuid: 'TEST_ALERT_2_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [true, true], - flapping: true, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_3: { - alertId: 'TEST_ALERT_3', - alertUuid: 'TEST_ALERT_3_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: notFlapping, - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect(serializedAlerts.state.trackedAlerts).toEqual({ - TEST_ALERT_2: { - activeCount: 0, - alertId: 'TEST_ALERT_2', - alertUuid: 'TEST_ALERT_2_UUID', - flapping: true, - flappingHistory: [true, true, true], - pendingRecoveredCount: 1, - started: '2020-01-02T12:00:00.000Z', - }, - }); - - expect(serializedAlerts.state.trackedAlertsRecovered).toEqual({ - TEST_ALERT_0: { - activeCount: 0, - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - flapping: true, - flappingHistory: [true, true, true, true, true], - pendingRecoveredCount: 0, - started: '2020-01-01T12:00:00.000Z', - }, - TEST_ALERT_1: { - activeCount: 0, - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - flapping: false, - flappingHistory: notFlapping.slice(0, notFlapping.length - 1).concat([true]), - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - TEST_ALERT_3: { - activeCount: 0, - alertId: 'TEST_ALERT_3', - alertUuid: 'TEST_ALERT_3_UUID', - flapping: false, - flappingHistory: notFlapping.slice(0, notFlapping.length - 1).concat([true]), - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - }); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // alert document - { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_FLAPPING]: false, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_FLAPPING]: false, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_FLAPPING]: true, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_FLAPPING]: false, - }), - ]), - }) - ); - }); - }); - - describe('set consecutive matches on the document', () => { - it('updates documents with consecutive matches for active alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'closed', - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', - [ALERT_UUID]: 'ALERT_2_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', - [ALERT_UUID]: 'ALERT_3_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: 'open', - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - services.alertWithLifecycle({ - id: 'TEST_ALERT_0', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_1', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_2', - fields: {}, - }); - services.alertWithLifecycle({ - id: 'TEST_ALERT_3', - fields: {}, - }); - - return { state }; - }); - - const serializedAlerts = await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_2: { - alertId: 'TEST_ALERT_2', - alertUuid: 'TEST_ALERT_2_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_3: { - alertId: 'TEST_ALERT_3', - alertUuid: 'TEST_ALERT_3_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect(serializedAlerts.state.trackedAlerts).toEqual({ - TEST_ALERT_0: { - activeCount: 1, - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - flapping: false, - flappingHistory: [false], - pendingRecoveredCount: 0, - started: '2020-01-01T12:00:00.000Z', - }, - TEST_ALERT_1: { - activeCount: 1, - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - flapping: false, - flappingHistory: [false], - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - TEST_ALERT_2: { - activeCount: 1, - alertId: 'TEST_ALERT_2', - alertUuid: 'TEST_ALERT_2_UUID', - flapping: false, - flappingHistory: [false], - pendingRecoveredCount: 0, - started: '2020-01-01T12:00:00.000Z', - }, - TEST_ALERT_3: { - activeCount: 1, - alertId: 'TEST_ALERT_3', - alertUuid: 'TEST_ALERT_3_UUID', - flapping: false, - flappingHistory: [false], - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - }); - - expect(serializedAlerts.state.trackedAlertsRecovered).toEqual({}); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: [ - // alert document - { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_WORKFLOW_STATUS]: 'closed', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [ALERT_CONSECUTIVE_MATCHES]: 1, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_CONSECUTIVE_MATCHES]: 1, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_CONSECUTIVE_MATCHES]: 1, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', - [ALERT_WORKFLOW_STATUS]: 'open', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [EVENT_ACTION]: 'active', - [EVENT_KIND]: 'signal', - [ALERT_CONSECUTIVE_MATCHES]: 1, - }), - ], - }) - ); - }); - - it('updates existing documents for recovered alerts', async () => { - const logger = loggerMock.create(); - const ruleDataClientMock = createRuleDataClientMock(); - ruleDataClientMock.getReader().search.mockResolvedValue({ - hits: { - hits: [ - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_UUID]: 'ALERT_0_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_UUID]: 'ALERT_1_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', - [ALERT_UUID]: 'ALERT_2_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - { - _source: { - '@timestamp': '', - [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', - [ALERT_UUID]: 'ALERT_3_UUID', - [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', - [ALERT_RULE_CONSUMER]: 'CONSUMER', - [ALERT_RULE_NAME]: 'NAME', - [ALERT_RULE_PRODUCER]: 'PRODUCER', - [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - [ALERT_RULE_UUID]: 'RULE_UUID', - [ALERT_STATUS]: ALERT_STATUS_ACTIVE, - [SPACE_IDS]: ['fake-space-id'], - }, - _index: '.alerts-index-name', - _seq_no: 4, - _primary_term: 2, - }, - ], - }, - } as any); - const executor = createLifecycleExecutor( - logger, - ruleDataClientMock - )<{}, TestRuleState, never, never, never>(async ({ services, state }) => { - return { state }; - }); - - const serializedAlerts = await executor( - createDefaultAlertExecutorOptions({ - alertId: 'TEST_ALERT_0', - params: {}, - state: { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_1: { - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_2: { - alertId: 'TEST_ALERT_2', - alertUuid: 'TEST_ALERT_2_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - TEST_ALERT_3: { - alertId: 'TEST_ALERT_3', - alertUuid: 'TEST_ALERT_3_UUID', - started: '2020-01-02T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }, - logger, - }) - ); - - expect(serializedAlerts.state.trackedAlerts).toEqual({}); - - expect(serializedAlerts.state.trackedAlertsRecovered).toEqual({ - TEST_ALERT_0: { - activeCount: 0, - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - flapping: false, - flappingHistory: [true], - pendingRecoveredCount: 0, - started: '2020-01-01T12:00:00.000Z', - }, - TEST_ALERT_1: { - activeCount: 0, - alertId: 'TEST_ALERT_1', - alertUuid: 'TEST_ALERT_1_UUID', - flapping: false, - flappingHistory: [true], - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - TEST_ALERT_2: { - activeCount: 0, - alertId: 'TEST_ALERT_2', - alertUuid: 'TEST_ALERT_2_UUID', - flapping: false, - flappingHistory: [true], - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - TEST_ALERT_3: { - activeCount: 0, - alertId: 'TEST_ALERT_3', - alertUuid: 'TEST_ALERT_3_UUID', - flapping: false, - flappingHistory: [true], - pendingRecoveredCount: 0, - started: '2020-01-02T12:00:00.000Z', - }, - }); - - expect((await ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledWith( - expect.objectContaining({ - body: expect.arrayContaining([ - // alert document - { index: expect.objectContaining({ _id: 'TEST_ALERT_0_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_CONSECUTIVE_MATCHES]: 0, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_1_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_CONSECUTIVE_MATCHES]: 0, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_2_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_2', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_CONSECUTIVE_MATCHES]: 0, - }), - { index: expect.objectContaining({ _id: 'TEST_ALERT_3_UUID' }) }, - expect.objectContaining({ - [ALERT_INSTANCE_ID]: 'TEST_ALERT_3', - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - [EVENT_ACTION]: 'close', - [EVENT_KIND]: 'signal', - [ALERT_CONSECUTIVE_MATCHES]: 0, - }), - ]), - }) - ); - }); - }); -}); - -type TestRuleState = Record & { - aRuleStateKey: string; -}; - -const initialRuleState: TestRuleState = { - aRuleStateKey: 'INITIAL_RULE_STATE_VALUE', -}; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts deleted file mode 100644 index cdbdf56fabc51..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ /dev/null @@ -1,479 +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 type { Logger } from '@kbn/logging'; -import type { PublicContract } from '@kbn/utility-types'; -import { getOrElse } from 'fp-ts/lib/Either'; -import { v4 } from 'uuid'; -import { difference } from 'lodash'; -import { - RuleExecutorOptions, - Alert, - AlertInstanceContext, - AlertInstanceState, - RuleTypeParams, - RuleTypeState, - isValidAlertIndexName, -} from '@kbn/alerting-plugin/server'; -import { isFlapping } from '@kbn/alerting-plugin/server/lib'; -import { wrappedStateRt, WrappedLifecycleRuleState } from '@kbn/alerting-state-types'; -export type { - TrackedLifecycleAlertState, - WrappedLifecycleRuleState, -} from '@kbn/alerting-state-types'; -import { ParsedExperimentalFields } from '../../common/parse_experimental_fields'; -import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; -import { - ALERT_TIME_RANGE, - ALERT_DURATION, - ALERT_END, - ALERT_INSTANCE_ID, - ALERT_START, - ALERT_STATUS, - ALERT_STATUS_ACTIVE, - ALERT_STATUS_RECOVERED, - ALERT_UUID, - ALERT_WORKFLOW_STATUS, - EVENT_ACTION, - EVENT_KIND, - TAGS, - TIMESTAMP, - VERSION, - ALERT_FLAPPING, - ALERT_MAINTENANCE_WINDOW_IDS, -} from '../../common/technical_rule_data_field_names'; -import { CommonAlertFieldNameLatest, CommonAlertIdFieldNameLatest } from '../../common/schemas'; -import { IRuleDataClient } from '../rule_data_client'; -import { AlertExecutorOptionsWithExtraServices } from '../types'; -import { fetchExistingAlerts } from './fetch_existing_alerts'; -import { getCommonAlertFields } from './get_common_alert_fields'; -import { getUpdatedFlappingHistory } from './get_updated_flapping_history'; -import { fetchAlertByAlertUUID } from './fetch_alert_by_uuid'; -import { getAlertsForNotification } from './get_alerts_for_notification'; - -type ImplicitTechnicalFieldName = CommonAlertFieldNameLatest | CommonAlertIdFieldNameLatest; - -type ExplicitTechnicalAlertFields = Partial< - Omit ->; - -type ExplicitAlertFields = Record & // every field can have values of arbitrary types - ExplicitTechnicalAlertFields; // but technical fields must obey their respective type - -export type LifecycleAlertService< - InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never, - ActionGroupIds extends string = never -> = (alert: { - id: string; - fields: ExplicitAlertFields; -}) => Alert; - -export interface LifecycleAlertServices< - InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never, - ActionGroupIds extends string = never -> { - alertWithLifecycle: LifecycleAlertService; - getAlertStartedDate: (alertInstanceId: string) => string | null; - getAlertUuid: (alertInstanceId: string) => string; - getAlertByAlertUuid: ( - alertUuid: string - ) => Promise | null> | null; -} - -export type LifecycleRuleExecutor< - Params extends RuleTypeParams = never, - State extends RuleTypeState = never, - InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never, - ActionGroupIds extends string = never -> = ( - options: AlertExecutorOptionsWithExtraServices< - Params, - State, - InstanceState, - InstanceContext, - ActionGroupIds, - LifecycleAlertServices - > -) => Promise<{ state: State }>; - -export const createLifecycleExecutor = - (logger: Logger, ruleDataClient: PublicContract) => - < - Params extends RuleTypeParams = never, - State extends RuleTypeState = never, - InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never, - ActionGroupIds extends string = never - >( - wrappedExecutor: LifecycleRuleExecutor< - Params, - State, - InstanceState, - InstanceContext, - ActionGroupIds - > - ) => - async ( - options: RuleExecutorOptions< - Params, - WrappedLifecycleRuleState, - InstanceState, - InstanceContext, - ActionGroupIds - > - ): Promise<{ state: WrappedLifecycleRuleState }> => { - const { - services: { alertFactory, getMaintenanceWindowIds, shouldWriteAlerts }, - state: previousState, - flappingSettings, - rule, - } = options; - - const ruleDataClientWriter = await ruleDataClient.getWriter(); - - const state = getOrElse( - (): WrappedLifecycleRuleState => ({ - wrapped: previousState as State, - trackedAlerts: {}, - trackedAlertsRecovered: {}, - }) - )(wrappedStateRt().decode(previousState)); - - const commonRuleFields = getCommonAlertFields(options); - - const currentAlerts: Record = {}; - const alertUuidMap: Map = new Map(); - - const lifecycleAlertServices: LifecycleAlertServices< - InstanceState, - InstanceContext, - ActionGroupIds - > = { - alertWithLifecycle: ({ id, fields }) => { - currentAlerts[id] = fields; - const alert = alertFactory.create(id); - const uuid = alert.getUuid(); - alertUuidMap.set(id, uuid); - return alert; - }, - getAlertStartedDate: (alertId: string) => state.trackedAlerts[alertId]?.started ?? null, - getAlertUuid: (alertId: string) => { - const uuid = alertUuidMap.get(alertId); - if (uuid) { - return uuid; - } - - const trackedAlert = state.trackedAlerts[alertId]; - if (trackedAlert) { - return trackedAlert.alertUuid; - } - - const trackedRecoveredAlert = state.trackedAlertsRecovered[alertId]; - if (trackedRecoveredAlert) { - return trackedRecoveredAlert.alertUuid; - } - - const alertInfo = `alert ${alertId} of rule ${rule.ruleTypeId}:${rule.id}`; - logger.warn( - `[Rule Registry] requesting uuid for ${alertInfo} which is not tracked, generating dynamically` - ); - return v4(); - }, - getAlertByAlertUuid: async (alertUuid: string) => { - try { - return await fetchAlertByAlertUUID(ruleDataClient, alertUuid); - } catch (err) { - return null; - } - }, - }; - - const wrappedExecutorResult = await wrappedExecutor({ - ...options, - state: state.wrapped != null ? state.wrapped : ({} as State), - services: { - ...options.services, - ...lifecycleAlertServices, - }, - }); - - const currentAlertIds = Object.keys(currentAlerts); - const trackedAlertIds = Object.keys(state.trackedAlerts); - const trackedAlertRecoveredIds = Object.keys(state.trackedAlertsRecovered); - const newAlertIds = difference(currentAlertIds, trackedAlertIds); - const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; - - const trackedAlertStates = Object.values(state.trackedAlerts); - - logger.debug( - `[Rule Registry] Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)` - ); - - // load maintenance window ids if there are new alerts - const maintenanceWindowIds: string[] = allAlertIds.length - ? await getMaintenanceWindowIds() - : []; - - interface TrackedAlertData { - indexName: string; - fields: Partial; - seqNo: number | undefined; - primaryTerm: number | undefined; - } - - const trackedAlertsDataMap: Record = {}; - - if (trackedAlertStates.length) { - const result = await fetchExistingAlerts( - ruleDataClient, - trackedAlertStates, - commonRuleFields - ); - result.forEach((hit) => { - const alertInstanceId = hit._source ? hit._source[ALERT_INSTANCE_ID] : void 0; - if (alertInstanceId && hit._source) { - const alertLabel = `${rule.ruleTypeId}:${rule.id} ${alertInstanceId}`; - if (hit._seq_no == null) { - logger.error(`missing _seq_no on alert instance ${alertLabel}`); - } else if (hit._primary_term == null) { - logger.error(`missing _primary_term on alert instance ${alertLabel}`); - } else { - trackedAlertsDataMap[alertInstanceId] = { - indexName: hit._index, - fields: hit._source, - seqNo: hit._seq_no, - primaryTerm: hit._primary_term, - }; - } - } - }); - } - - const makeEventsDataMapFor = (alertIds: string[]) => - alertIds - .filter((alertId) => { - const alertData = trackedAlertsDataMap[alertId]; - const alertIndex = alertData?.indexName; - if (!alertIndex) { - return true; - } else if (!isValidAlertIndexName(alertIndex)) { - logger.warn( - `Could not update alert ${alertId} in ${alertIndex}. Partial and restored alert indices are not supported.` - ); - return false; - } - return true; - }) - .map((alertId) => { - const alertData = trackedAlertsDataMap[alertId]; - const currentAlertData = currentAlerts[alertId]; - const trackedAlert = state.trackedAlerts[alertId]; - - if (!alertData) { - logger.debug(`[Rule Registry] Could not find alert data for ${alertId}`); - } - - const isNew = !trackedAlert; - const isRecovered = !currentAlertData; - const isActive = !isRecovered; - - const flappingHistory = getUpdatedFlappingHistory( - flappingSettings, - alertId, - state, - isNew, - isRecovered, - isActive, - trackedAlertRecoveredIds - ); - - const { alertUuid, started, flapping, pendingRecoveredCount, activeCount } = !isNew - ? state.trackedAlerts[alertId] - : { - alertUuid: lifecycleAlertServices.getAlertUuid(alertId), - started: commonRuleFields[TIMESTAMP], - flapping: state.trackedAlertsRecovered[alertId] - ? state.trackedAlertsRecovered[alertId].flapping - : false, - pendingRecoveredCount: 0, - activeCount: 0, - }; - - const event: ParsedTechnicalFields & ParsedExperimentalFields = { - ...alertData?.fields, - ...commonRuleFields, - ...currentAlertData, - [ALERT_DURATION]: (options.startedAt.getTime() - new Date(started).getTime()) * 1000, - [ALERT_TIME_RANGE]: isRecovered - ? { - gte: started, - lte: commonRuleFields[TIMESTAMP], - } - : { gte: started }, - [ALERT_INSTANCE_ID]: alertId, - [ALERT_START]: started, - [ALERT_UUID]: alertUuid, - [ALERT_STATUS]: isRecovered ? ALERT_STATUS_RECOVERED : ALERT_STATUS_ACTIVE, - [ALERT_WORKFLOW_STATUS]: alertData?.fields[ALERT_WORKFLOW_STATUS] ?? 'open', - [EVENT_KIND]: 'signal', - [EVENT_ACTION]: isNew ? 'open' : isActive ? 'active' : 'close', - [TAGS]: Array.from( - new Set([ - ...(currentAlertData?.tags ?? []), - ...(alertData?.fields[TAGS] ?? []), - ...(options.rule.tags ?? []), - ]) - ), - [VERSION]: ruleDataClient.kibanaVersion, - [ALERT_FLAPPING]: flapping, - ...(isRecovered ? { [ALERT_END]: commonRuleFields[TIMESTAMP] } : {}), - ...(isNew && maintenanceWindowIds?.length - ? { [ALERT_MAINTENANCE_WINDOW_IDS]: maintenanceWindowIds } - : {}), - }; - - return { - indexName: alertData?.indexName, - seqNo: alertData?.seqNo, - primaryTerm: alertData?.primaryTerm, - event, - flappingHistory, - flapping, - pendingRecoveredCount, - activeCount, - }; - }); - - const trackedEventsToIndex = makeEventsDataMapFor(trackedAlertIds); - const newEventsToIndex = makeEventsDataMapFor(newAlertIds); - const trackedRecoveredEventsToIndex = makeEventsDataMapFor(trackedAlertRecoveredIds); - const allEventsToIndex = getAlertsForNotification( - flappingSettings, - rule.alertDelay?.active ?? 0, - trackedEventsToIndex, - newEventsToIndex, - { maintenanceWindowIds, timestamp: commonRuleFields[TIMESTAMP] } - ); - - // Only write alerts if: - // - writing is enabled - // AND - // - rule execution has not been cancelled due to timeout - // OR - // - if execution has been cancelled due to timeout, if feature flags are configured to write alerts anyway - const writeAlerts = ruleDataClient.isWriteEnabled() && shouldWriteAlerts(); - - if (allEventsToIndex.length > 0 && writeAlerts) { - logger.debug(`[Rule Registry] Preparing to index ${allEventsToIndex.length} alerts.`); - - await ruleDataClientWriter.bulk({ - body: allEventsToIndex.flatMap(({ event, indexName, seqNo, primaryTerm }) => [ - indexName - ? { - index: { - _id: event[ALERT_UUID]!, - _index: indexName, - if_seq_no: seqNo, - if_primary_term: primaryTerm, - require_alias: false, - }, - } - : { - create: { - _id: event[ALERT_UUID]!, - }, - }, - event, - ]), - refresh: true, - }); - } else { - logger.debug( - `[Rule Registry] Not indexing ${allEventsToIndex.length} alerts because writing has been disabled.` - ); - } - - const nextTrackedAlerts = Object.fromEntries( - [...newEventsToIndex, ...trackedEventsToIndex] - .filter(({ event }) => event[ALERT_STATUS] !== ALERT_STATUS_RECOVERED) - .map( - ({ - event, - flappingHistory, - flapping: isCurrentlyFlapping, - pendingRecoveredCount, - activeCount, - }) => { - const alertId = event[ALERT_INSTANCE_ID]!; - const alertUuid = event[ALERT_UUID]!; - const started = new Date(event[ALERT_START]!).toISOString(); - const flapping = isFlapping(flappingSettings, flappingHistory, isCurrentlyFlapping); - return [ - alertId, - { - alertId, - alertUuid, - started, - flappingHistory, - flapping, - pendingRecoveredCount, - activeCount, - }, - ]; - } - ) - ); - - const nextTrackedAlertsRecovered = Object.fromEntries( - [...allEventsToIndex, ...trackedRecoveredEventsToIndex] - .filter( - ({ event, flappingHistory, flapping }) => - // return recovered alerts if they are flapping or if the flapping array is not at capacity - // this is a space saving effort that will stop tracking a recovered alert if it wasn't flapping and doesn't have state changes - // in the last max capcity number of executions - event[ALERT_STATUS] === ALERT_STATUS_RECOVERED && - (flapping || flappingHistory.filter((f: boolean) => f).length > 0) - ) - .map( - ({ - event, - flappingHistory, - flapping: isCurrentlyFlapping, - pendingRecoveredCount, - activeCount, - }) => { - const alertId = event[ALERT_INSTANCE_ID]!; - const alertUuid = event[ALERT_UUID]!; - const started = new Date(event[ALERT_START]!).toISOString(); - const flapping = isFlapping(flappingSettings, flappingHistory, isCurrentlyFlapping); - return [ - alertId, - { - alertId, - alertUuid, - started, - flappingHistory, - flapping, - pendingRecoveredCount, - activeCount, - }, - ]; - } - ) - ); - - return { - state: { - wrapped: wrappedExecutorResult?.state ?? ({} as State), - trackedAlerts: writeAlerts ? nextTrackedAlerts : {}, - trackedAlertsRecovered: writeAlerts ? nextTrackedAlertsRecovered : {}, - }, - }; - }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts deleted file mode 100644 index bf0d98d5156af..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_executor_mock.ts +++ /dev/null @@ -1,38 +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 { - RuleTypeParams, - RuleTypeState, - AlertInstanceState, - AlertInstanceContext, -} from '@kbn/alerting-plugin/server'; -import { AlertExecutorOptionsWithExtraServices } from '../types'; - -import { LifecycleAlertServices, LifecycleRuleExecutor } from './create_lifecycle_executor'; - -export const createLifecycleRuleExecutorMock = - < - Params extends RuleTypeParams = never, - State extends RuleTypeState = never, - InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never, - ActionGroupIds extends string = never - >( - executor: LifecycleRuleExecutor - ) => - async ( - options: AlertExecutorOptionsWithExtraServices< - Params, - State, - InstanceState, - InstanceContext, - ActionGroupIds, - LifecycleAlertServices - > - ) => - await executor(options); diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts deleted file mode 100644 index 6dbc33b666497..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ /dev/null @@ -1,512 +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 { schema } from '@kbn/config-schema'; -import { - ALERT_DURATION, - ALERT_STATUS, - ALERT_STATUS_ACTIVE, - ALERT_STATUS_RECOVERED, - ALERT_UUID, - ALERT_TIME_RANGE, -} from '@kbn/rule-data-utils'; -import { loggerMock } from '@kbn/logging-mocks'; -import { castArray, omit } from 'lodash'; -import { createRuleDataClientMock } from '../rule_data_client/rule_data_client.mock'; -import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_factory'; -import { ISearchStartSearchSource } from '@kbn/data-plugin/common'; -import { SharePluginStart } from '@kbn/share-plugin/server'; -import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; -import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common/rules_settings'; - -type RuleTestHelpers = ReturnType; - -function createRule(shouldWriteAlerts: boolean = true) { - const ruleDataClientMock = createRuleDataClientMock(); - - const factory = createLifecycleRuleTypeFactory({ - ruleDataClient: ruleDataClientMock, - logger: loggerMock.create(), - }); - - let nextAlerts: Array<{ id: string; fields: Record }> = []; - - const type = factory({ - actionGroups: [ - { - id: 'warning', - name: 'warning', - }, - ], - actionVariables: { - context: [], - params: [], - state: [], - }, - defaultActionGroupId: 'warning', - executor: async ({ services }) => { - nextAlerts.forEach((alert) => { - services.alertWithLifecycle(alert); - }); - nextAlerts = []; - return { state: {} }; - }, - id: 'ruleTypeId', - isExportable: true, - minimumLicenseRequired: 'basic', - name: 'ruleTypeName', - category: 'test', - producer: 'producer', - validate: { - params: schema.object( - {}, - { - unknowns: 'allow', - } - ), - }, - }); - - let state: Record = {}; - let previousStartedAt: Date | null; - const createdAt = new Date('2021-06-16T09:00:00.000Z'); - - const scheduleActions = jest.fn(); - - let uuidCounter = 1; - const getUuid = jest.fn(() => `uuid-${uuidCounter++}`); - - const alertFactory = { - create: () => { - return { - scheduleActions, - getUuid, - } as any; - }, - alertLimit: { - getValue: () => 1000, - setLimitReached: () => {}, - }, - done: () => ({ getRecoveredAlerts: () => [] }), - }; - - return { - alertWithLifecycle: async (alerts: Array<{ id: string; fields: Record }>) => { - nextAlerts = alerts; - - const startedAt = new Date((previousStartedAt ?? createdAt).getTime() + 60000); - - scheduleActions.mockClear(); - - ({ state } = ((await type.executor({ - executionId: 'b33f65d7-6e8b-4aae-8d20-c93613dec9f9', - logger: loggerMock.create(), - namespace: 'namespace', - params: { threshold: 1, operator: '>' }, - previousStartedAt, - rule: { - id: 'alertId', - actions: [], - consumer: 'consumer', - createdAt, - createdBy: 'createdBy', - enabled: true, - muteAll: false, - name: 'name', - notifyWhen: 'onActionGroupChange', - producer: 'producer', - revision: 0, - ruleTypeId: 'ruleTypeId', - ruleTypeName: 'ruleTypeName', - schedule: { - interval: '1m', - }, - snoozeSchedule: [], - tags: ['tags'], - throttle: null, - updatedAt: createdAt, - updatedBy: 'updatedBy', - }, - services: { - alertsClient: null, - alertFactory, - savedObjectsClient: {} as any, - scopedClusterClient: {} as any, - search: {} as any, - getMaintenanceWindowIds: async () => [], - getSearchSourceClient: async () => ({} as ISearchStartSearchSource), - shouldStopExecution: () => false, - shouldWriteAlerts: () => shouldWriteAlerts, - uiSettingsClient: {} as any, - share: {} as SharePluginStart, - getDataViews: async () => dataViewPluginMocks.createStartContract(), - }, - spaceId: 'spaceId', - startedAt, - startedAtOverridden: false, - state, - flappingSettings: DEFAULT_FLAPPING_SETTINGS, - getTimeRange: () => { - const date = new Date(Date.now()).toISOString(); - return { dateStart: date, dateEnd: date }; - }, - })) ?? {}) as Record); - - previousStartedAt = startedAt; - }, - scheduleActions, - ruleDataClientMock, - }; -} - -describe('createLifecycleRuleTypeFactory', () => { - describe('with a new rule', () => { - let helpers: RuleTestHelpers; - - beforeEach(() => { - helpers = createRule(); - }); - - describe('when writing is disabled', () => { - beforeEach(() => { - helpers.ruleDataClientMock.isWriteEnabled.mockReturnValue(false); - }); - - it("doesn't persist anything", async () => { - await helpers.alertWithLifecycle([ - { - id: 'opbeans-java', - fields: { - 'service.name': 'opbeans-java', - }, - }, - ]); - - expect((await helpers.ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledTimes(0); - }); - }); - - describe('when rule is cancelled due to timeout and config flags indicate to skip actions', () => { - beforeEach(() => { - helpers = createRule(false); - helpers.ruleDataClientMock.isWriteEnabled.mockReturnValue(true); - }); - - it("doesn't persist anything", async () => { - await helpers.alertWithLifecycle([ - { - id: 'opbeans-java', - fields: { - 'service.name': 'opbeans-java', - }, - }, - ]); - - expect((await helpers.ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledTimes(0); - }); - }); - - describe('when alerts are new', () => { - beforeEach(async () => { - await helpers.alertWithLifecycle([ - { - id: 'opbeans-java', - fields: { - 'service.name': 'opbeans-java', - }, - }, - { - id: 'opbeans-node', - fields: { - 'service.name': 'opbeans-node', - }, - }, - ]); - }); - - it('writes the correct alerts', async () => { - expect((await helpers.ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledTimes(1); - - const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[0][0].body!; - - const documents: any[] = body.filter((op: any) => !isOpDoc(op)); - - const evaluationDocuments = documents.filter((doc) => doc['event.kind'] === 'event'); - const alertDocuments = documents.filter((doc) => doc['event.kind'] === 'signal'); - - expect(evaluationDocuments.length).toBe(0); - expect(alertDocuments.length).toBe(2); - - expect( - alertDocuments.every((doc) => doc[ALERT_STATUS] === ALERT_STATUS_ACTIVE) - ).toBeTruthy(); - - expect(alertDocuments.every((doc) => doc[ALERT_DURATION] === 0)).toBeTruthy(); - - expect(alertDocuments.every((doc) => doc['event.action'] === 'open')).toBeTruthy(); - - expect(documents.map((doc) => omit(doc, ALERT_UUID))).toMatchInlineSnapshot(` - Array [ - Object { - "@timestamp": "2021-06-16T09:01:00.000Z", - "event.action": "open", - "event.kind": "signal", - "kibana.alert.consecutive_matches": 1, - "kibana.alert.duration.us": 0, - "kibana.alert.flapping": false, - "kibana.alert.instance.id": "opbeans-java", - "kibana.alert.rule.category": "ruleTypeName", - "kibana.alert.rule.consumer": "consumer", - "kibana.alert.rule.execution.uuid": "b33f65d7-6e8b-4aae-8d20-c93613dec9f9", - "kibana.alert.rule.name": "name", - "kibana.alert.rule.parameters": Object { - "operator": ">", - "threshold": 1, - }, - "kibana.alert.rule.producer": "producer", - "kibana.alert.rule.revision": 0, - "kibana.alert.rule.rule_type_id": "ruleTypeId", - "kibana.alert.rule.tags": Array [ - "tags", - ], - "kibana.alert.rule.uuid": "alertId", - "kibana.alert.start": "2021-06-16T09:01:00.000Z", - "kibana.alert.status": "active", - "kibana.alert.time_range": Object { - "gte": "2021-06-16T09:01:00.000Z", - }, - "kibana.alert.workflow_status": "open", - "kibana.space_ids": Array [ - "spaceId", - ], - "kibana.version": "7.16.0", - "service.name": "opbeans-java", - "tags": Array [ - "tags", - ], - }, - Object { - "@timestamp": "2021-06-16T09:01:00.000Z", - "event.action": "open", - "event.kind": "signal", - "kibana.alert.consecutive_matches": 1, - "kibana.alert.duration.us": 0, - "kibana.alert.flapping": false, - "kibana.alert.instance.id": "opbeans-node", - "kibana.alert.rule.category": "ruleTypeName", - "kibana.alert.rule.consumer": "consumer", - "kibana.alert.rule.execution.uuid": "b33f65d7-6e8b-4aae-8d20-c93613dec9f9", - "kibana.alert.rule.name": "name", - "kibana.alert.rule.parameters": Object { - "operator": ">", - "threshold": 1, - }, - "kibana.alert.rule.producer": "producer", - "kibana.alert.rule.revision": 0, - "kibana.alert.rule.rule_type_id": "ruleTypeId", - "kibana.alert.rule.tags": Array [ - "tags", - ], - "kibana.alert.rule.uuid": "alertId", - "kibana.alert.start": "2021-06-16T09:01:00.000Z", - "kibana.alert.status": "active", - "kibana.alert.time_range": Object { - "gte": "2021-06-16T09:01:00.000Z", - }, - "kibana.alert.workflow_status": "open", - "kibana.space_ids": Array [ - "spaceId", - ], - "kibana.version": "7.16.0", - "service.name": "opbeans-node", - "tags": Array [ - "tags", - ], - }, - ] - `); - }); - }); - - describe('when alerts are active', () => { - beforeEach(async () => { - await helpers.alertWithLifecycle([ - { - id: 'opbeans-java', - fields: { - 'service.name': 'opbeans-java', - }, - }, - { - id: 'opbeans-node', - fields: { - 'service.name': 'opbeans-node', - }, - }, - ]); - - // TODO mock the resolved value before calling alertWithLifecycle again - const lastOpbeansNodeDoc = ( - await helpers.ruleDataClientMock.getWriter() - ).bulk.mock.calls[0][0].body - ?.concat() - .reverse() - .find((doc: any) => !isOpDoc(doc) && doc['service.name'] === 'opbeans-node') as Record< - string, - any - >; - - // @ts-ignore 4.3.5 upgrade - helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ - hits: { - hits: [{ _source: lastOpbeansNodeDoc } as any], - total: { - value: 1, - relation: 'eq', - }, - }, - took: 0, - timed_out: false, - _shards: { - failed: 0, - successful: 1, - total: 1, - }, - }); - - await helpers.alertWithLifecycle([ - { - id: 'opbeans-java', - fields: { - 'service.name': 'opbeans-java', - }, - }, - { - id: 'opbeans-node', - fields: { - 'service.name': 'opbeans-node', - 'kibana.alert.workflow_status': 'closed', - }, - }, - ]); - }); - - it('writes the correct alerts', async () => { - expect((await helpers.ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledTimes(2); - const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[1][0].body!; - - const documents: any[] = body.filter((op: any) => !isOpDoc(op)); - - const evaluationDocuments = documents.filter((doc) => doc['event.kind'] === 'event'); - const alertDocuments = documents.filter((doc) => doc['event.kind'] === 'signal'); - - expect(evaluationDocuments.length).toBe(0); - expect(alertDocuments.length).toBe(2); - - expect( - alertDocuments.every((doc) => doc[ALERT_STATUS] === ALERT_STATUS_ACTIVE) - ).toBeTruthy(); - expect(alertDocuments.every((doc) => doc['event.action'] === 'active')).toBeTruthy(); - - expect(alertDocuments.every((doc) => doc[ALERT_DURATION] > 0)).toBeTruthy(); - }); - }); - - describe('when alerts recover', () => { - beforeEach(async () => { - await helpers.alertWithLifecycle([ - { - id: 'opbeans-java', - fields: { - 'service.name': 'opbeans-java', - }, - }, - { - id: 'opbeans-node', - fields: { - 'service.name': 'opbeans-node', - }, - }, - ]); - - const lastOpbeansNodeDoc = ( - await helpers.ruleDataClientMock.getWriter() - ).bulk.mock.calls[0][0].body - ?.concat() - .reverse() - .find((doc: any) => !isOpDoc(doc) && doc['service.name'] === 'opbeans-node') as Record< - string, - any - >; - - helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ - hits: { - hits: [ - { - _source: lastOpbeansNodeDoc, - _index: '.alerts-a', - _primary_term: 4, - _seq_no: 2, - } as any, - ], - total: { - value: 1, - relation: 'eq', - }, - }, - took: 0, - timed_out: false, - _shards: { - failed: 0, - successful: 1, - total: 1, - }, - }); - - await helpers.alertWithLifecycle([ - { - id: 'opbeans-java', - fields: { - 'service.name': 'opbeans-java', - }, - }, - ]); - }); - - it('writes the correct alerts', async () => { - expect((await helpers.ruleDataClientMock.getWriter()).bulk).toHaveBeenCalledTimes(2); - - const body = (await helpers.ruleDataClientMock.getWriter()).bulk.mock.calls[1][0].body!; - - const documents: any[] = body.filter((op: any) => !isOpDoc(op)); - - const opbeansJavaAlertDoc = documents.find( - (doc) => castArray(doc['service.name'])[0] === 'opbeans-java' - ); - const opbeansNodeAlertDoc = documents.find( - (doc) => castArray(doc['service.name'])[0] === 'opbeans-node' - ); - - expect(opbeansJavaAlertDoc['event.action']).toBe('active'); - expect(opbeansJavaAlertDoc[ALERT_STATUS]).toBe(ALERT_STATUS_ACTIVE); - - expect(opbeansNodeAlertDoc['event.action']).toBe('close'); - expect(opbeansNodeAlertDoc[ALERT_STATUS]).toBe(ALERT_STATUS_RECOVERED); - expect(opbeansNodeAlertDoc[ALERT_TIME_RANGE]).toEqual({ - gte: '2021-06-16T09:01:00.000Z', - lte: '2021-06-16T09:02:00.000Z', - }); - }); - }); - }); -}); - -function isOpDoc(doc: any) { - if (doc?.index?._id) return true; - if (doc?.create?._id) return true; - return false; -} diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts deleted file mode 100644 index 7f1be5ff54f83..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts +++ /dev/null @@ -1,45 +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 { Logger } from '@kbn/logging'; -import { - AlertInstanceContext, - AlertInstanceState, - RuleTypeParams, - RuleTypeState, -} from '@kbn/alerting-plugin/common'; -import { IRuleDataClient } from '../rule_data_client'; -import { AlertTypeWithExecutor } from '../types'; -import { createLifecycleExecutor, LifecycleAlertServices } from './create_lifecycle_executor'; - -export const createLifecycleRuleTypeFactory = - ({ logger, ruleDataClient }: { logger: Logger; ruleDataClient: IRuleDataClient }) => - < - TParams extends RuleTypeParams, - TAlertInstanceState extends AlertInstanceState, - TAlertInstanceContext extends AlertInstanceContext, - TActionGroupIds extends string, - TServices extends LifecycleAlertServices< - TAlertInstanceState, - TAlertInstanceContext, - TActionGroupIds - > - >( - type: AlertTypeWithExecutor - ): AlertTypeWithExecutor => { - const createBoundLifecycleExecutor = createLifecycleExecutor(logger, ruleDataClient); - const executor = createBoundLifecycleExecutor< - TParams, - RuleTypeState, - AlertInstanceState, - TAlertInstanceContext, - string - >(type.executor as any); - return { - ...type, - executor: executor as any, - }; - }; diff --git a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts deleted file mode 100644 index 84685779186d9..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.test.ts +++ /dev/null @@ -1,207 +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 { - DEFAULT_FLAPPING_SETTINGS, - DISABLE_FLAPPING_SETTINGS, -} from '@kbn/alerting-plugin/common/rules_settings'; -import { getUpdatedFlappingHistory } from './get_updated_flapping_history'; - -describe('getUpdatedFlappingHistory', () => { - type TestRuleState = Record & { - aRuleStateKey: string; - }; - const initialRuleState: TestRuleState = { - aRuleStateKey: 'INITIAL_RULE_STATE_VALUE', - }; - - test('sets flapping state to true if the alert is new', () => { - const state = { wrapped: initialRuleState, trackedAlerts: {}, trackedAlertsRecovered: {} }; - expect( - getUpdatedFlappingHistory( - DEFAULT_FLAPPING_SETTINGS, - 'TEST_ALERT_0', - state, - true, - false, - false, - [] - ) - ).toMatchInlineSnapshot(` - Array [ - true, - ] - `); - }); - - test('sets flapping state to false on an alert that is still active', () => { - const state = { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }; - expect( - getUpdatedFlappingHistory( - DEFAULT_FLAPPING_SETTINGS, - 'TEST_ALERT_0', - state, - false, - false, - true, - [] - ) - ).toMatchInlineSnapshot(` - Array [ - false, - ] - `); - }); - - test('sets flapping state to true on an alert that is active and previously recovered', () => { - const state = { - wrapped: initialRuleState, - trackedAlertsRecovered: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlerts: {}, - }; - const recoveredIds = ['TEST_ALERT_0']; - expect( - getUpdatedFlappingHistory( - DEFAULT_FLAPPING_SETTINGS, - 'TEST_ALERT_0', - state, - true, - false, - true, - recoveredIds - ) - ).toMatchInlineSnapshot(` - Array [ - true, - ] - `); - expect(recoveredIds).toEqual([]); - }); - - test('sets flapping state to true on an alert that is recovered and previously active', () => { - const state = { - wrapped: initialRuleState, - trackedAlerts: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - trackedAlertsRecovered: {}, - }; - const recoveredIds = ['TEST_ALERT_0']; - expect( - getUpdatedFlappingHistory( - DEFAULT_FLAPPING_SETTINGS, - 'TEST_ALERT_0', - state, - false, - true, - false, - recoveredIds - ) - ).toMatchInlineSnapshot(` - Array [ - true, - ] - `); - expect(recoveredIds).toEqual(['TEST_ALERT_0']); - }); - - test('sets flapping state to false on an alert that is still recovered', () => { - const state = { - wrapped: initialRuleState, - trackedAlerts: {}, - trackedAlertsRecovered: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - }; - const recoveredIds = ['TEST_ALERT_0']; - expect( - getUpdatedFlappingHistory( - DEFAULT_FLAPPING_SETTINGS, - 'TEST_ALERT_0', - state, - false, - true, - false, - recoveredIds - ) - ).toMatchInlineSnapshot(` - Array [ - false, - ] - `); - expect(recoveredIds).toEqual(['TEST_ALERT_0']); - }); - - test('does not set flapping state if flapping is not enabled', () => { - const state = { - wrapped: initialRuleState, - trackedAlerts: {}, - trackedAlertsRecovered: { - TEST_ALERT_0: { - alertId: 'TEST_ALERT_0', - alertUuid: 'TEST_ALERT_0_UUID', - started: '2020-01-01T12:00:00.000Z', - flappingHistory: [], - flapping: false, - pendingRecoveredCount: 0, - activeCount: 0, - }, - }, - }; - expect( - getUpdatedFlappingHistory( - DISABLE_FLAPPING_SETTINGS, - 'TEST_ALERT_0', - state, - false, - true, - false, - ['TEST_ALERT_0'] - ) - ).toMatchInlineSnapshot(`Array []`); - }); -}); diff --git a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts b/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts deleted file mode 100644 index 854f919722330..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/get_updated_flapping_history.ts +++ /dev/null @@ -1,64 +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 { RuleTypeState } from '@kbn/alerting-plugin/common'; -import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common/rules_settings'; -import { updateFlappingHistory } from '@kbn/alerting-plugin/server/lib'; -import { remove } from 'lodash'; -import { WrappedLifecycleRuleState } from './create_lifecycle_executor'; - -export function getUpdatedFlappingHistory( - flappingSettings: RulesSettingsFlappingProperties, - alertId: string, - state: WrappedLifecycleRuleState, - isNew: boolean, - isRecovered: boolean, - isActive: boolean, - recoveredIds: string[] -) { - // duplicating this logic to determine flapping at this level - let flappingHistory: boolean[] = []; - if (flappingSettings.enabled) { - if (isRecovered) { - if (state.trackedAlerts[alertId]) { - // this alert has flapped from active to recovered - flappingHistory = updateFlappingHistory( - flappingSettings, - state.trackedAlerts[alertId].flappingHistory, - true - ); - } else if (state.trackedAlertsRecovered[alertId]) { - // this alert is still recovered - flappingHistory = updateFlappingHistory( - flappingSettings, - state.trackedAlertsRecovered[alertId].flappingHistory, - false - ); - } - } else if (isNew) { - if (state.trackedAlertsRecovered[alertId]) { - // this alert has flapped from recovered to active - flappingHistory = updateFlappingHistory( - flappingSettings, - state.trackedAlertsRecovered[alertId].flappingHistory, - true - ); - remove(recoveredIds, (id) => id === alertId); - } else { - flappingHistory = updateFlappingHistory(flappingSettings, [], true); - } - } else if (isActive) { - // this alert is still active - flappingHistory = updateFlappingHistory( - flappingSettings, - state.trackedAlerts[alertId].flappingHistory, - false - ); - } - } - return flappingHistory; -} diff --git a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts b/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts deleted file mode 100644 index 9324bcfd76cb4..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/lifecycle_alert_services.mock.ts +++ /dev/null @@ -1,41 +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 { AlertInstanceContext, AlertInstanceState } from '@kbn/alerting-plugin/server'; -import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; -import { LifecycleAlertServices } from './create_lifecycle_executor'; - -/** - * This wraps the alerts to enable the preservation of the generic type - * arguments of the factory function. - **/ -class AlertsMockWrapper< - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext -> { - createAlertServices() { - return alertsMock.createRuleExecutorServices(); - } -} - -type AlertServices< - InstanceState extends AlertInstanceState = AlertInstanceState, - InstanceContext extends AlertInstanceContext = AlertInstanceContext -> = ReturnType['createAlertServices']>; - -export const createLifecycleAlertServicesMock = < - InstanceState extends AlertInstanceState = never, - InstanceContext extends AlertInstanceContext = never, - ActionGroupIds extends string = never ->( - alertServices: AlertServices -): LifecycleAlertServices => ({ - alertWithLifecycle: ({ id }) => alertServices.alertFactory.create(id), - getAlertStartedDate: jest.fn((id: string) => null), - getAlertUuid: jest.fn((id: string) => 'mock-alert-uuid'), - getAlertByAlertUuid: jest.fn((id: string) => Promise.resolve(null)), -}); diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index 71f1e13a199b5..8c244ed95e014 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -34,7 +34,6 @@ "@kbn/alerts-as-data-utils", "@kbn/core-http-router-server-mocks", "@kbn/core-http-server", - "@kbn/alerting-state-types", "@kbn/alerting-types" ], "exclude": [ diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts index 0700cba718324..6c16c694bc9e9 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/rule_types.ts @@ -17,7 +17,6 @@ import { RuleTypeState, RuleTypeParams, } from '@kbn/alerting-plugin/server'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import { ES_TEST_INDEX_NAME } from '@kbn/alerting-api-integration-helpers'; import { FixtureStartDeps, FixtureSetupDeps } from './plugin'; @@ -884,33 +883,14 @@ function getCancellableRuleType() { return result; } -function getAlwaysFiringAlertAsDataRuleType( - logger: Logger, - { ruleRegistry }: Pick -) { +function getAlwaysFiringAlertAsDataRuleType() { const paramsSchema = schema.object({ index: schema.string(), reference: schema.string(), }); + type ParamsType = TypeOf; - const ruleDataClient = ruleRegistry.ruleDataService.initializeIndex({ - feature: AlertConsumers.OBSERVABILITY, - registrationContext: 'observability.test.alerts', - dataset: ruleRegistry.dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - }, - ], - }); - - const createLifecycleRuleType = ruleRegistry.createLifecycleRuleTypeFactory({ - logger, - ruleDataClient, - }); - - return createLifecycleRuleType({ + const result: RuleType = { id: 'test.always-firing-alert-as-data', name: 'Test: Always Firing Alert As Data', actionGroups: [{ id: 'default', name: 'Default' }], @@ -926,19 +906,8 @@ function getAlwaysFiringAlertAsDataRuleType( const { services, params, state, spaceId, namespace, rule } = ruleExecutorOptions; const ruleInfo = { spaceId, namespace, ...rule }; - services - .alertWithLifecycle({ - id: '1', - fields: {}, - }) - .scheduleActions('default'); - - services - .alertWithLifecycle({ - id: '2', - fields: {}, - }) - .scheduleActions('default'); + services.alertsClient?.report({ id: '1', actionGroup: 'default' }); + services.alertsClient?.report({ id: '2', actionGroup: 'default' }); await services.scopedClusterClient.asCurrentUser.index({ index: params.index, @@ -960,8 +929,10 @@ function getAlwaysFiringAlertAsDataRuleType( fieldMap: {}, }, useLegacyAlerts: true, + shouldWrite: true, }, - }); + }; + return result; } function getWaitingRuleType(logger: Logger) { @@ -1393,7 +1364,7 @@ export function defineRuleTypes( alerting.registerType(getCancellableRuleType()); alerting.registerType(getPatternSuccessOrFailureRuleType()); alerting.registerType(getExceedsAlertLimitRuleType()); - alerting.registerType(getAlwaysFiringAlertAsDataRuleType(logger, { ruleRegistry })); + alerting.registerType(getAlwaysFiringAlertAsDataRuleType()); alerting.registerType(getPatternFiringAutoRecoverFalseRuleType()); alerting.registerType(getPatternFiringAlertsAsDataRuleType()); alerting.registerType(getWaitingRuleType(logger)); diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/tsconfig.json b/x-pack/test/alerting_api_integration/common/plugins/alerts/tsconfig.json index 9088008908f1b..e7fe45ad6fe11 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/tsconfig.json +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/tsconfig.json @@ -24,7 +24,6 @@ "@kbn/notifications-plugin", "@kbn/core-saved-objects-server", "@kbn/logging", - "@kbn/rule-data-utils", "@kbn/alerting-api-integration-helpers", ], "exclude": [ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_untrack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_untrack.ts index 8157c71aef3b8..2fe1aceb60132 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_untrack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_untrack.ts @@ -40,6 +40,7 @@ export default function bulkUntrackTests({ getService }: FtrProviderContext) { match_all: {}, }, conflicts: 'proceed', + ignore_unavailable: true, }); await objectRemover.removeAll(); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_untrack_by_query.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_untrack_by_query.ts index c7062bd478911..794cb73677730 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_untrack_by_query.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/bulk_untrack_by_query.ts @@ -31,6 +31,7 @@ export default function bulkUntrackByQueryTests({ getService }: FtrProviderConte match_all: {}, }, conflicts: 'proceed', + ignore_unavailable: true, }); await objectRemover.removeAll(); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts index a1c645f7a4122..c3cc9f410b203 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group4/tests/alerting/alerts.ts @@ -51,7 +51,11 @@ export default function alertTests({ getService }: FtrProviderContext) { after(async () => { await esTestIndexTool.destroy(); await es.indices.delete({ index: authorizationIndex }); - await es.deleteByQuery({ index: alertAsDataIndex, query: { match_all: {} } }); + await es.deleteByQuery({ + index: alertAsDataIndex, + query: { match_all: {} }, + ignore_unavailable: true, + }); }); for (const scenario of UserAtSpaceScenarios) { @@ -1493,6 +1497,10 @@ instanceStateValue: true _index: '.internal.alerts-observability.test.alerts.alerts-default-000001', kibana: { alert: { + action_group: 'default', + flapping_history: expectExpect.any(Array), + maintenance_window_ids: [], + severity_improving: false, rule: { parameters: { index: '.kibana-alerting-test-data', @@ -1500,7 +1508,10 @@ instanceStateValue: true }, category: 'Test: Always Firing Alert As Data', consumer: 'alertsFixture', - execution: { uuid: expectExpect.any(String) }, + execution: { + uuid: expectExpect.any(String), + timestamp: expectExpect.any(String), + }, name: 'abc', producer: 'alertsFixture', revision: 0, @@ -1530,6 +1541,10 @@ instanceStateValue: true _index: '.internal.alerts-observability.test.alerts.alerts-default-000001', kibana: { alert: { + action_group: 'default', + flapping_history: expectExpect.any(Array), + maintenance_window_ids: [], + severity_improving: false, rule: { parameters: { index: '.kibana-alerting-test-data', @@ -1537,7 +1552,10 @@ instanceStateValue: true }, category: 'Test: Always Firing Alert As Data', consumer: 'alertsFixture', - execution: { uuid: expectExpect.any(String) }, + execution: { + timestamp: expectExpect.any(String), + uuid: expectExpect.any(String), + }, name: 'abc', producer: 'alertsFixture', revision: 0, @@ -1583,6 +1601,10 @@ instanceStateValue: true _index: '.internal.alerts-observability.test.alerts.alerts-default-000001', kibana: { alert: { + action_group: 'default', + flapping_history: expectExpect.any(Array), + maintenance_window_ids: [], + previous_action_group: 'default', rule: { parameters: { index: '.kibana-alerting-test-data', @@ -1590,7 +1612,10 @@ instanceStateValue: true }, category: 'Test: Always Firing Alert As Data', consumer: 'alertsFixture', - execution: { uuid: expectExpect.any(String) }, + execution: { + timestamp: expectExpect.any(String), + uuid: expectExpect.any(String), + }, name: 'abc', producer: 'alertsFixture', revision: 0, @@ -1620,6 +1645,10 @@ instanceStateValue: true _index: '.internal.alerts-observability.test.alerts.alerts-default-000001', kibana: { alert: { + action_group: 'default', + flapping_history: expectExpect.any(Array), + maintenance_window_ids: [], + previous_action_group: 'default', rule: { parameters: { index: '.kibana-alerting-test-data', @@ -1627,7 +1656,10 @@ instanceStateValue: true }, category: 'Test: Always Firing Alert As Data', consumer: 'alertsFixture', - execution: { uuid: expectExpect.any(String) }, + execution: { + timestamp: expectExpect.any(String), + uuid: expectExpect.any(String), + }, name: 'abc', producer: 'alertsFixture', revision: 0, @@ -1721,6 +1753,10 @@ instanceStateValue: true _index: '.internal.alerts-observability.test.alerts.alerts-default-000001', kibana: { alert: { + action_group: 'default', + flapping_history: expectExpect.any(Array), + maintenance_window_ids: [], + severity_improving: false, rule: { parameters: { index: '.kibana-alerting-test-data', @@ -1728,7 +1764,10 @@ instanceStateValue: true }, category: 'Test: Always Firing Alert As Data', consumer: 'alertsFixture', - execution: { uuid: expectExpect.any(String) }, + execution: { + uuid: expectExpect.any(String), + timestamp: expectExpect.any(String), + }, name: 'abc', producer: 'alertsFixture', revision: 0, @@ -1758,6 +1797,10 @@ instanceStateValue: true _index: '.internal.alerts-observability.test.alerts.alerts-default-000001', kibana: { alert: { + action_group: 'default', + flapping_history: expectExpect.any(Array), + maintenance_window_ids: [], + severity_improving: false, rule: { parameters: { index: '.kibana-alerting-test-data', @@ -1765,7 +1808,10 @@ instanceStateValue: true }, category: 'Test: Always Firing Alert As Data', consumer: 'alertsFixture', - execution: { uuid: expectExpect.any(String) }, + execution: { + uuid: expectExpect.any(String), + timestamp: expectExpect.any(String), + }, name: 'abc', producer: 'alertsFixture', revision: 0, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts index 87d6f7e8da59f..01fa746b65f2a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/disable.ts @@ -43,6 +43,7 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex match_all: {}, }, conflicts: 'proceed', + ignore_unavailable: true, }); await objectRemover.removeAll(); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_alert_delay.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_alert_delay.ts index 9665e112facbc..59068ee945ea2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_alert_delay.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/alerts_as_data/alerts_as_data_alert_delay.ts @@ -34,7 +34,7 @@ import { ALERT_CONSECUTIVE_MATCHES, } from '@kbn/rule-data-utils'; import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; -import { ES_TEST_INDEX_NAME, ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; +import { ESTestIndexTool } from '@kbn/alerting-api-integration-helpers'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { Spaces } from '../../../../scenarios'; import { @@ -79,6 +79,7 @@ export default function createAlertsAsDataAlertDelayInstallResourcesTest({ index: [alertsAsDataIndex, alwaysFiringAlertsAsDataIndex], query: { match_all: {} }, conflicts: 'proceed', + ignore_unavailable: true, }); }); afterEach(async () => { @@ -87,6 +88,7 @@ export default function createAlertsAsDataAlertDelayInstallResourcesTest({ index: [alertsAsDataIndex, alwaysFiringAlertsAsDataIndex], query: { match_all: {} }, conflicts: 'proceed', + ignore_unavailable: true, }); }); after(async () => { @@ -395,234 +397,6 @@ export default function createAlertsAsDataAlertDelayInstallResourcesTest({ expect(alertDocsRun6.length).to.equal(1); }); - it('should generate expected events with a alertDelay with AAD (rule registry)', async () => { - const params = { - index: ES_TEST_INDEX_NAME, - reference: 'test', - }; - const { body: createdAction } = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'foo') - .send({ - name: 'MY action', - connector_type_id: 'test.noop', - config: {}, - secrets: {}, - }) - .expect(200); - - const response = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - rule_type_id: 'test.always-firing-alert-as-data', - schedule: { interval: '1d' }, - throttle: null, - notify_when: null, - params, - actions: [ - { - id: createdAction.id, - group: 'default', - params: {}, - frequency: { - summary: false, - throttle: null, - notify_when: RuleNotifyWhen.CHANGE, - }, - }, - ], - alert_delay: { - active: 3, - }, - }) - ); - - expect(response.status).to.eql(200); - const ruleId = response.body.id; - objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting'); - - // -------------------------- - // RUN 1 - 0 new alerts - // -------------------------- - let events: IValidatedEvent[] = await waitForEventLogDocs( - ruleId, - new Map([['execute', { equal: 1 }]]) - ); - let executeEvent = events[0]; - expect(get(executeEvent, ACTIVE_PATH)).to.be(0); - expect(get(executeEvent, NEW_PATH)).to.be(0); - expect(get(executeEvent, RECOVERED_PATH)).to.be(0); - expect(get(executeEvent, ACTION_PATH)).to.be(0); - expect(get(executeEvent, DELAYED_PATH)).to.be(2); - - // Query for alerts - const alertDocsRun1 = await queryForAlertDocs(alwaysFiringAlertsAsDataIndex); - - // Get alert state from task document - let state: any = await getTaskState(ruleId); - expect(state.alertInstances['1'].meta.activeCount).to.equal(1); - expect(state.alertTypeState.trackedAlerts['1'].activeCount).to.equal(1); - - // After the first run, we should have 0 alert docs for the 0 active alerts - expect(alertDocsRun1.length).to.equal(0); - - // -------------------------- - // RUN 2 - 0 new alerts - // -------------------------- - let runSoon = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${ruleId}/_run_soon`) - .set('kbn-xsrf', 'foo'); - expect(runSoon.status).to.eql(204); - - events = await waitForEventLogDocs(ruleId, new Map([['execute', { equal: 2 }]])); - executeEvent = events[1]; - expect(get(executeEvent, ACTIVE_PATH)).to.be(0); - expect(get(executeEvent, NEW_PATH)).to.be(0); - expect(get(executeEvent, RECOVERED_PATH)).to.be(0); - expect(get(executeEvent, ACTION_PATH)).to.be(0); - expect(get(executeEvent, DELAYED_PATH)).to.be(2); - - // Query for alerts - const alertDocsRun2 = await queryForAlertDocs(alwaysFiringAlertsAsDataIndex); - - // Get alert state from task document - state = await getTaskState(ruleId); - expect(state.alertInstances['1'].meta.activeCount).to.equal(2); - expect(state.alertTypeState.trackedAlerts['1'].activeCount).to.equal(2); - - // After the second run, we should have 0 alert docs for the 0 active alerts - expect(alertDocsRun2.length).to.equal(0); - - // -------------------------- - // RUN 3 - 1 new alert - // -------------------------- - runSoon = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${ruleId}/_run_soon`) - .set('kbn-xsrf', 'foo'); - expect(runSoon.status).to.eql(204); - - events = await waitForEventLogDocs(ruleId, new Map([['execute', { equal: 3 }]])); - executeEvent = events[2]; - let executionUuid = get(executeEvent, UUID_PATH); - // Note: the rule creates 2 alerts but we will only look at one - expect(get(executeEvent, ACTIVE_PATH)).to.be(2); - expect(get(executeEvent, NEW_PATH)).to.be(2); - expect(get(executeEvent, RECOVERED_PATH)).to.be(0); - expect(get(executeEvent, ACTION_PATH)).to.be(2); - expect(get(executeEvent, DELAYED_PATH)).to.be(0); - - // Query for alerts - const alertDocsRun3 = await queryForAlertDocs(alwaysFiringAlertsAsDataIndex); - - // Get alert state from task document - state = await getTaskState(ruleId); - expect(state.alertInstances['1'].meta.activeCount).to.equal(3); - expect(state.alertTypeState.trackedAlerts['1'].activeCount).to.equal(3); - - // After the third run, we should have 2 alert docs for the 2 active alerts but we will only look at one - expect(alertDocsRun3.length).to.equal(2); - - let source: Alert = alertDocsRun3[0]._source!; - - // Each doc should have a copy of the rule data - expect(source[ALERT_RULE_CATEGORY]).to.equal('Test: Always Firing Alert As Data'); - expect(source[ALERT_RULE_CONSUMER]).to.equal('alertsFixture'); - expect(source[ALERT_RULE_NAME]).to.equal('abc'); - expect(source[ALERT_RULE_PRODUCER]).to.equal('alertsFixture'); - expect(source[ALERT_RULE_TAGS]).to.eql(['foo']); - expect(source[ALERT_RULE_TYPE_ID]).to.equal('test.always-firing-alert-as-data'); - expect(source[ALERT_RULE_UUID]).to.equal(ruleId); - expect(source[ALERT_RULE_PARAMETERS]).to.eql(params); - expect(source[SPACE_IDS]).to.eql(['space1']); - expect(source[ALERT_RULE_EXECUTION_UUID]).to.equal(executionUuid); - // alert UUID should equal doc id - expect(source[ALERT_UUID]).to.equal(alertDocsRun3[0]._id); - // duration should be 0 since this is a new alert - expect(source[ALERT_DURATION]).to.equal(0); - // start should be defined - expect(source[ALERT_START]).to.match(timestampPattern); - // time_range.gte should be same as start - expect(source[ALERT_TIME_RANGE]?.gte).to.equal(source[ALERT_START]); - // timestamp should be defined - expect(source['@timestamp']).to.match(timestampPattern); - // status should be active - expect(source[ALERT_STATUS]).to.equal('active'); - // workflow status should be 'open' - expect(source[ALERT_WORKFLOW_STATUS]).to.equal('open'); - // event.action should be 'open' - expect(source[EVENT_ACTION]).to.equal('open'); - // event.kind should be 'signal' - expect(source[EVENT_KIND]).to.equal('signal'); - // tags should equal rule tags because rule type doesn't set any tags - expect(source.tags).to.eql(['foo']); - // alert consecutive matches should match the active count - expect(source[ALERT_CONSECUTIVE_MATCHES]).to.equal(3); - - // -------------------------- - // RUN 4 - 1 active alert - // -------------------------- - runSoon = await supertestWithoutAuth - .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${ruleId}/_run_soon`) - .set('kbn-xsrf', 'foo'); - expect(runSoon.status).to.eql(204); - - events = await waitForEventLogDocs(ruleId, new Map([['execute', { equal: 4 }]])); - executeEvent = events[3]; - executionUuid = get(executeEvent, UUID_PATH); - // Note: the rule creates 2 alerts but we will only look at one - expect(get(executeEvent, ACTIVE_PATH)).to.be(2); - expect(get(executeEvent, NEW_PATH)).to.be(0); - expect(get(executeEvent, RECOVERED_PATH)).to.be(0); - expect(get(executeEvent, ACTION_PATH)).to.be(0); - expect(get(executeEvent, DELAYED_PATH)).to.be(0); - - // Query for alerts - const alertDocsRun4 = await queryForAlertDocs(alwaysFiringAlertsAsDataIndex); - - // Get alert state from task document - state = await getTaskState(ruleId); - expect(state.alertInstances['1'].meta.activeCount).to.equal(4); - expect(state.alertTypeState.trackedAlerts['1'].activeCount).to.equal(4); - - // After the fourth run, we should have 2 alert docs for the 2 active alerts but we will only look at one - expect(alertDocsRun4.length).to.equal(2); - - source = alertDocsRun4[0]._source!; - const run3Source = alertDocsRun3[0]._source!; - - // Each doc should have a copy of the rule data - expect(source[ALERT_RULE_CATEGORY]).to.equal('Test: Always Firing Alert As Data'); - expect(source[ALERT_RULE_CONSUMER]).to.equal('alertsFixture'); - expect(source[ALERT_RULE_NAME]).to.equal('abc'); - expect(source[ALERT_RULE_PRODUCER]).to.equal('alertsFixture'); - expect(source[ALERT_RULE_TAGS]).to.eql(['foo']); - expect(source[ALERT_RULE_TYPE_ID]).to.equal('test.always-firing-alert-as-data'); - expect(source[ALERT_RULE_UUID]).to.equal(ruleId); - expect(source[ALERT_RULE_PARAMETERS]).to.eql(params); - expect(source[SPACE_IDS]).to.eql(['space1']); - expect(source[ALERT_RULE_EXECUTION_UUID]).to.equal(executionUuid); - expect(source[ALERT_UUID]).to.equal(run3Source[ALERT_UUID]); - // start time should be defined and the same as prior run - expect(source[ALERT_START]).to.match(timestampPattern); - expect(source[ALERT_START]).to.equal(run3Source[ALERT_START]); - // timestamp should be defined and not the same as prior run - expect(source['@timestamp']).to.match(timestampPattern); - expect(source['@timestamp']).not.to.equal(run3Source['@timestamp']); - // status should still be active - expect(source[ALERT_STATUS]).to.equal('active'); - // event.action set to active - expect(source[EVENT_ACTION]).to.eql('active'); - expect(source.tags).to.eql(['foo']); - // these values should be the same as previous run - expect(source[EVENT_KIND]).to.eql(run3Source[EVENT_KIND]); - expect(source[ALERT_WORKFLOW_STATUS]).to.eql(run3Source[ALERT_WORKFLOW_STATUS]); - expect(source[ALERT_TIME_RANGE]?.gte).to.equal(run3Source[ALERT_TIME_RANGE]?.gte); - // alert consecutive matches should match the active count - expect(source[ALERT_CONSECUTIVE_MATCHES]).to.equal(4); - }); - it('should not recover alert if the activeCount did not reach the alertDelay threshold with AAD', async () => { const { body: createdAction } = await supertestWithoutAuth .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts index e5692e73a15ab..8cf9de5fd3057 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/bulk_disable.ts @@ -63,6 +63,7 @@ export default function createDisableRuleTests({ getService }: FtrProviderContex match_all: {}, }, conflicts: 'proceed', + ignore_unavailable: true, }); await objectRemover.removeAll(); }); diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts index a8c8421815eec..964139be03bb4 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts @@ -23,6 +23,5 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { // Trial loadTestFile(require.resolve('./get_alert_by_id')); loadTestFile(require.resolve('./update_alert')); - loadTestFile(require.resolve('./lifecycle_executor')); }); }; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts deleted file mode 100644 index 9f6eebedd7e7c..0000000000000 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/lifecycle_executor.ts +++ /dev/null @@ -1,275 +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. - */ - -// WARNING: This test running in Function Test Runner is building a live -// LifecycleRuleExecutor, feeding it some mock data, but letting it write -// it's various alerts to indices. I suspect it's quite fragile, and I -// added this comment to fix some fragility in the way the alert factory -// was built. I suspect it will suffer more such things in the future. -// I fixed this as a drive-by, but opened an issue to do something later, -// if needed: https://github.com/elastic/kibana/issues/144557 - -import { type Subject, ReplaySubject, of } from 'rxjs'; -import type { ElasticsearchClient, Logger, LogMeta } from '@kbn/core/server'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { mappingFromFieldMap } from '@kbn/alerting-plugin/common'; -import { - AlertConsumers, - ALERT_REASON, - ALERT_UUID, -} from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names'; -import { - createLifecycleExecutor, - WrappedLifecycleRuleState, -} from '@kbn/rule-registry-plugin/server/utils/create_lifecycle_executor'; -import { Dataset, IRuleDataClient, RuleDataService } from '@kbn/rule-registry-plugin/server'; -import { RuleExecutorOptions } from '@kbn/alerting-plugin/server'; -import { getDataStreamAdapter } from '@kbn/alerting-plugin/server/alerts_service/lib/data_stream_adapter'; -import type { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { - MockRuleParams, - MockRuleState, - MockAlertContext, - MockAlertState, - MockAllowedActionGroups, -} from '../../../common/types'; -import { cleanupRegistryIndices, getMockAlertFactory } from '../../../common/lib/helpers'; - -// eslint-disable-next-line import/no-default-export -export default function createLifecycleExecutorApiTest({ getService }: FtrProviderContext) { - const es = getService('es'); - const log = getService('log'); - - const fakeLogger = (msg: string, meta?: Meta) => - meta ? log.debug(msg, meta) : log.debug(msg); - - const logger = { - trace: fakeLogger, - debug: fakeLogger, - info: fakeLogger, - warn: fakeLogger, - error: fakeLogger, - fatal: fakeLogger, - log: sinon.stub(), - get: sinon.stub(), - isLevelEnabled: sinon.stub(), - } as Logger; - - const getClusterClient = () => { - const client = es as ElasticsearchClient; - return Promise.resolve(client); - }; - - const dataStreamAdapter = getDataStreamAdapter({ useDataStreamForAlerts: false }); - - describe('createLifecycleExecutor', () => { - let ruleDataClient: IRuleDataClient; - let pluginStop$: Subject; - const elasticsearchAndSOAvailability$ = of(true); - - before(async () => { - // First we need to setup the data service. This happens within the - // Rule Registry plugin as part of the server side setup phase. - pluginStop$ = new ReplaySubject(1); - - const ruleDataService = new RuleDataService({ - getClusterClient, - logger, - kibanaVersion: '8.0.0', - isWriteEnabled: true, - isWriterCacheEnabled: false, - disabledRegistrationContexts: [] as string[], - frameworkAlerts: { - enabled: () => false, - getContextInitializationPromise: async () => ({ result: false }), - }, - pluginStop$, - dataStreamAdapter, - elasticsearchAndSOAvailability$, - }); - - // This initializes the service. This happens immediately after the creation - // of the RuleDataService in the setup phase of the Rule Registry plugin - ruleDataService.initializeService(); - - // This initializes the index and templates and returns the data client. - // This happens in each solution plugin before they can register lifecycle - // executors. - ruleDataClient = ruleDataService.initializeIndex({ - feature: AlertConsumers.OBSERVABILITY, - registrationContext: 'observability.test', - dataset: Dataset.alerts, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - mappings: mappingFromFieldMap( - { - testObject: { - type: 'object', - required: false, - array: false, - }, - }, - false - ), - }, - ], - }); - }); - - after(async () => { - cleanupRegistryIndices(getService, ruleDataClient); - pluginStop$.next(); - pluginStop$.complete(); - }); - - it('should work with object fields', async () => { - const id = 'host-01'; - - // This creates the function that will wrap the solution's rule executor with the RuleRegistry lifecycle - const createLifecycleRuleExecutor = createLifecycleExecutor(logger, ruleDataClient); - - // This creates the executor that is passed to the Alerting framework. - const executor = createLifecycleRuleExecutor< - MockRuleParams, - MockRuleState, - MockAlertState, - MockAlertContext, - MockAllowedActionGroups - >(async function (options) { - const { services, state: previousState } = options; - const { alertWithLifecycle } = services; - - // Fake some state updates - const state = previousState.testObject - ? { - ...previousState, - testObject: { - ...previousState.testObject, - values: [ - ...previousState.testObject.values, - { name: 'count', value: previousState.testObject.values.length + 1 }, - ], - }, - } - : { - ...previousState, - testObject: { - id, - values: [{ name: 'count', value: 1 }], - host: { - name: id, - }, - }, - }; - - // This MUST be called by the solutions executor function - alertWithLifecycle({ - id, - fields: { - [ALERT_REASON]: 'Test alert is firing', - ...state, - }, - }); - - // Returns the current state of the alert - return Promise.resolve({ state }); - }); - - const ruleId = 'rule-id'; - // Create the options with the minimal amount of values to test the lifecycle executor - const options = { - alertId: ruleId, - spaceId: 'default', - tags: ['test'], - startedAt: new Date(), - rule: { - id: ruleId, - name: 'test rule', - ruleTypeId: 'observability.test.fake', - ruleTypeName: 'test', - consumer: 'observability', - producer: 'observability.test', - }, - services: { - alertFactory: getMockAlertFactory(), - shouldWriteAlerts: sinon.stub().returns(true), - getMaintenanceWindowIds: async () => [], - }, - flappingSettings: { - enabled: false, - lookBackWindow: 20, - statusChangeThreshold: 4, - }, - dataStreamAdapter, - } as unknown as RuleExecutorOptions< - MockRuleParams, - WrappedLifecycleRuleState, - { [x: string]: unknown }, - { [x: string]: unknown }, - string - >; - - // Execute the rule the first time - const executorResult = await executor(options); - expect(executorResult.state.wrapped).to.eql({ - testObject: { - host: { name: 'host-01' }, - id: 'host-01', - values: [{ name: 'count', value: 1 }], - }, - }); - - const alertUuid = executorResult.state.trackedAlerts['host-01'].alertUuid; - expect(alertUuid).to.be('uuid-1'); - - // We need to refresh the index so the data is available for the next call - await es.indices.refresh({ index: `${ruleDataClient.indexName}*` }); - - // Execute again to ensure that we read the object and write it again with the updated state - const nextExecutorResult = await executor({ ...options, state: executorResult.state }); - expect(nextExecutorResult.state.wrapped).to.eql({ - testObject: { - host: { name: 'host-01' }, - id: 'host-01', - values: [ - { name: 'count', value: 1 }, - { name: 'count', value: 2 }, - ], - }, - }); - - // Refresh again so we can query the data to check it was written properly - await es.indices.refresh({ index: `${ruleDataClient.indexName}*` }); - - // Use the ruleDataClient to read the results from the index - const response = await ruleDataClient.getReader().search({ - body: { - query: { - bool: { - filter: [ - { - term: { - [ALERT_UUID]: nextExecutorResult.state.trackedAlerts['host-01'].alertUuid, - }, - }, - ], - }, - }, - }, - }); - const source = response.hits.hits[0]._source as any; - - // The state in Elasticsearch should match the state returned from the executor - expect(source.testObject).to.eql( - nextExecutorResult.state.wrapped && nextExecutorResult.state.wrapped.testObject - ); - }); - }); -}