diff --git a/packages/kbn-rule-data-utils/src/alerts_as_data_status.ts b/packages/kbn-rule-data-utils/src/alerts_as_data_status.ts new file mode 100644 index 0000000000000..cb36ce339e79a --- /dev/null +++ b/packages/kbn-rule-data-utils/src/alerts_as_data_status.ts @@ -0,0 +1,12 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const ALERT_STATUS_ACTIVE = 'active'; +export const ALERT_STATUS_RECOVERED = 'recovered'; + +export type AlertStatus = typeof ALERT_STATUS_ACTIVE | typeof ALERT_STATUS_RECOVERED; diff --git a/packages/kbn-rule-data-utils/src/index.ts b/packages/kbn-rule-data-utils/src/index.ts index ef06d5777b5ab..a08216e59401c 100644 --- a/packages/kbn-rule-data-utils/src/index.ts +++ b/packages/kbn-rule-data-utils/src/index.ts @@ -9,3 +9,4 @@ export * from './technical_field_names'; export * from './alerts_as_data_rbac'; export * from './alerts_as_data_severity'; +export * from './alerts_as_data_status'; diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index 6e2323bb4c54b..45a8dd842ee27 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -6,11 +6,12 @@ */ import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { IndexPatternBase } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; +import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; import React, { useCallback, useRef } from 'react'; import { useHistory } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; -import { IndexPatternBase } from '@kbn/es-query'; import { ParsedTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; import type { AlertWorkflowStatus } from '../../../common/typings'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; @@ -21,8 +22,8 @@ import { RouteParams } from '../../routes'; import { callObservabilityApi } from '../../services/call_observability_api'; import { AlertsSearchBar } from './alerts_search_bar'; import { AlertsTableTGrid } from './alerts_table_t_grid'; -import { WorkflowStatusFilter } from './workflow_status_filter'; import './styles.scss'; +import { WorkflowStatusFilter } from './workflow_status_filter'; export interface TopAlert { fields: ParsedTechnicalFields; @@ -45,7 +46,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { query: { rangeFrom = 'now-15m', rangeTo = 'now', - kuery = 'kibana.alert.status: "open"', // TODO change hardcoded values as part of another PR + kuery = `${ALERT_STATUS}: "${ALERT_STATUS_ACTIVE}"`, workflowStatus = 'open', }, } = routeParams; diff --git a/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts b/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts index 4e99bdb0ee32d..6b4240c9ad346 100644 --- a/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts +++ b/x-pack/plugins/observability/public/pages/alerts/parse_alert.ts @@ -18,6 +18,7 @@ import { ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; +import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; import type { TopAlert } from '.'; import { parseTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; import { asDuration, asPercent } from '../../../common/utils/formatters'; @@ -42,7 +43,7 @@ export const parseAlert = (observabilityRuleTypeRegistry: ObservabilityRuleTypeR return { ...formatted, fields: parsedFields, - active: parsedFields[ALERT_STATUS] !== 'closed', + active: parsedFields[ALERT_STATUS] === ALERT_STATUS_ACTIVE, start: new Date(parsedFields[ALERT_START] ?? 0).getTime(), }; }; diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index 691bfc984b9cb..0430c750c8862 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -26,7 +26,7 @@ import { TIMESTAMP, // @ts-expect-error importing from a place other than root because we want to limit what we import from this package } from '@kbn/rule-data-utils/target_node/technical_field_names'; - +import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common'; import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; import { asDuration } from '../../../common/utils/formatters'; @@ -82,7 +82,7 @@ export const getRenderCellValue = ({ switch (columnId) { case ALERT_STATUS: switch (value) { - case 'open': + case ALERT_STATUS_ACTIVE: return ( {i18n.translate('xpack.observability.alertsTGrid.statusActiveDescription', { @@ -90,7 +90,7 @@ export const getRenderCellValue = ({ })} ); - case 'closed': + case ALERT_STATUS_RECOVERED: return ( diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index 29f03024b79f5..03d96a24bedd3 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -18,15 +18,15 @@ export const technicalRuleFieldMap = { ), [Fields.ALERT_RULE_TYPE_ID]: { type: 'keyword', required: true }, [Fields.ALERT_RULE_CONSUMER]: { type: 'keyword', required: true }, - [Fields.ALERT_RULE_PRODUCER]: { type: 'keyword' }, + [Fields.ALERT_RULE_PRODUCER]: { type: 'keyword', required: true }, [Fields.SPACE_IDS]: { type: 'keyword', array: true, required: true }, - [Fields.ALERT_UUID]: { type: 'keyword' }, - [Fields.ALERT_ID]: { type: 'keyword' }, + [Fields.ALERT_UUID]: { type: 'keyword', required: true }, + [Fields.ALERT_ID]: { type: 'keyword', required: true }, [Fields.ALERT_START]: { type: 'date' }, [Fields.ALERT_END]: { type: 'date' }, [Fields.ALERT_DURATION]: { type: 'long' }, [Fields.ALERT_SEVERITY]: { type: 'keyword' }, - [Fields.ALERT_STATUS]: { type: 'keyword' }, + [Fields.ALERT_STATUS]: { type: 'keyword', required: true }, [Fields.ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 }, [Fields.ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 }, [Fields.VERSION]: { @@ -87,12 +87,12 @@ export const technicalRuleFieldMap = { [Fields.ALERT_RULE_CATEGORY]: { type: 'keyword', array: false, - required: false, + required: true, }, [Fields.ALERT_RULE_UUID]: { type: 'keyword', array: false, - required: false, + required: true, }, [Fields.ALERT_RULE_CREATED_AT]: { type: 'date', @@ -132,7 +132,7 @@ export const technicalRuleFieldMap = { [Fields.ALERT_RULE_NAME]: { type: 'keyword', array: false, - required: false, + required: true, }, [Fields.ALERT_RULE_NOTE]: { type: 'keyword', diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 2a609aa3bef7e..e49b2a4d5abed 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -19,7 +19,6 @@ export * from './config'; export * from './rule_data_plugin_service'; export * from './rule_data_client'; -export { getRuleData, RuleExecutorData } from './utils/get_rule_executor_data'; export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; export { LifecycleRuleExecutor, diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts index 372fb09661259..d8640cf5dfe82 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts @@ -6,16 +6,22 @@ */ import { + ALERT_ID, + ALERT_RULE_CATEGORY, ALERT_RULE_CONSUMER, + ALERT_RULE_NAME, + ALERT_RULE_PRODUCER, ALERT_RULE_RISK_SCORE, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, ALERT_STATUS, + ALERT_STATUS_ACTIVE, + ALERT_UUID, ECS_VERSION, - ALERT_RULE_TYPE_ID, SPACE_IDS, TIMESTAMP, VERSION, } from '@kbn/rule-data-utils'; - import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; import { getAlertByIdRoute } from './get_alert_by_id'; @@ -24,14 +30,20 @@ import { getReadRequest } from './__mocks__/request_responses'; import { requestMock, serverMock } from './__mocks__/server'; const getMockAlert = (): ParsedTechnicalFields => ({ - [TIMESTAMP]: '2021-06-21T21:33:05.713Z', - [ECS_VERSION]: '1.0.0', - [VERSION]: '7.13.0', - [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_ID]: 'fake-alert-id', + [ALERT_RULE_CATEGORY]: 'apm.error_rate', [ALERT_RULE_CONSUMER]: 'apm', - [ALERT_STATUS]: 'open', + [ALERT_RULE_NAME]: 'Check error rate', + [ALERT_RULE_PRODUCER]: 'apm', [ALERT_RULE_RISK_SCORE]: 20, + [ALERT_RULE_TYPE_ID]: 'fake-rule-type-id', + [ALERT_RULE_UUID]: 'fake-rule-uuid', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_UUID]: 'fake-alert-uuid', + [ECS_VERSION]: '1.0.0', [SPACE_IDS]: ['fake-space-id'], + [TIMESTAMP]: '2021-06-21T21:33:05.713Z', + [VERSION]: '7.13.0', }); describe('getAlertByIdRoute', () => { 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 index 2d0ca3e328a13..c1a4fccaf205b 100644 --- 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 @@ -6,29 +6,25 @@ */ import { loggerMock } from '@kbn/logging/mocks'; -import { - elasticsearchServiceMock, - savedObjectsClientMock, -} from '../../../../../src/core/server/mocks'; -import { - AlertExecutorOptions, - AlertInstanceContext, - AlertInstanceState, - AlertTypeParams, - AlertTypeState, -} from '../../../alerting/server'; -import { alertsMock } from '../../../alerting/server/mocks'; import { ALERT_ID, + 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_UUID, EVENT_ACTION, EVENT_KIND, - ALERT_RULE_TYPE_ID, - ALERT_RULE_CONSUMER, SPACE_IDS, } 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_utils'; describe('createLifecycleExecutor', () => { it('wraps and unwraps the original executor state', async () => { @@ -95,14 +91,14 @@ describe('createLifecycleExecutor', () => { { index: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: 'open', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', }), { index: { _id: expect.any(String) } }, expect.objectContaining({ [ALERT_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: 'open', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [EVENT_ACTION]: 'open', [EVENT_KIND]: 'signal', }), @@ -192,14 +188,14 @@ describe('createLifecycleExecutor', () => { { index: { _id: 'TEST_ALERT_0_UUID' } }, expect.objectContaining({ [ALERT_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: 'open', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), { index: { _id: 'TEST_ALERT_1_UUID' } }, expect.objectContaining({ [ALERT_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: 'open', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), @@ -220,6 +216,8 @@ describe('createLifecycleExecutor', () => { }); it('updates existing documents for recovered alerts', async () => { + // NOTE: the documents should actually also be updated for recurring, + // active alerts (see elastic/kibana#108670) const logger = loggerMock.create(); const ruleDataClientMock = createRuleDataClientMock(); ruleDataClientMock.getReader().search.mockResolvedValue({ @@ -229,8 +227,14 @@ describe('createLifecycleExecutor', () => { fields: { '@timestamp': '', [ALERT_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 }, @@ -239,8 +243,14 @@ describe('createLifecycleExecutor', () => { fields: { '@timestamp': '', [ALERT_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 }, @@ -290,7 +300,7 @@ describe('createLifecycleExecutor', () => { { index: { _id: 'TEST_ALERT_0_UUID' } }, expect.objectContaining({ [ALERT_ID]: 'TEST_ALERT_0', - [ALERT_STATUS]: 'closed', + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, [EVENT_ACTION]: 'close', [EVENT_KIND]: 'signal', @@ -298,7 +308,7 @@ describe('createLifecycleExecutor', () => { { index: { _id: 'TEST_ALERT_1_UUID' } }, expect.objectContaining({ [ALERT_ID]: 'TEST_ALERT_1', - [ALERT_STATUS]: 'open', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), @@ -326,62 +336,3 @@ type TestRuleState = Record & { const initialRuleState: TestRuleState = { aRuleStateKey: 'INITIAL_RULE_STATE_VALUE', }; - -const createDefaultAlertExecutorOptions = < - Params extends AlertTypeParams = never, - State extends AlertTypeState = never, - InstanceState extends AlertInstanceState = {}, - InstanceContext extends AlertInstanceContext = {}, - ActionGroupIds extends string = '' ->({ - alertId = 'ALERT_ID', - ruleName = 'ALERT_RULE_NAME', - params, - state, - createdAt = new Date(), - startedAt = new Date(), - updatedAt = new Date(), -}: { - alertId?: string; - ruleName?: string; - params: Params; - state: State; - createdAt?: Date; - startedAt?: Date; - updatedAt?: Date; -}): AlertExecutorOptions => ({ - alertId, - createdBy: 'CREATED_BY', - startedAt, - name: ruleName, - rule: { - updatedBy: null, - tags: [], - name: ruleName, - createdBy: null, - actions: [], - enabled: true, - consumer: 'CONSUMER', - producer: 'ALERT_PRODUCER', - schedule: { interval: '1m' }, - throttle: null, - createdAt, - updatedAt, - notifyWhen: null, - ruleTypeId: 'RULE_TYPE_ID', - ruleTypeName: 'RULE_TYPE_NAME', - }, - tags: [], - params, - spaceId: 'SPACE_ID', - services: { - alertInstanceFactory: alertsMock.createAlertServices() - .alertInstanceFactory, - savedObjectsClient: savedObjectsClientMock.create(), - scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - }, - state, - updatedBy: null, - previousStartedAt: null, - namespace: undefined, -}); 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 index a3e830d6e0b2f..97337e3a5e09e 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -9,7 +9,6 @@ import type { Logger } from '@kbn/logging'; import type { PublicContract } from '@kbn/utility-types'; import { getOrElse } from 'fp-ts/lib/Either'; import * as rt from 'io-ts'; -import { Mutable } from 'utility-types'; import { v4 } from 'uuid'; import { AlertExecutorOptions, @@ -24,22 +23,34 @@ import { ALERT_DURATION, ALERT_END, ALERT_ID, - ALERT_RULE_CONSUMER, - ALERT_RULE_TYPE_ID, ALERT_RULE_UUID, ALERT_START, ALERT_STATUS, + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, ALERT_UUID, ALERT_WORKFLOW_STATUS, EVENT_ACTION, EVENT_KIND, - SPACE_IDS, TIMESTAMP, VERSION, } from '../../common/technical_rule_data_field_names'; import { IRuleDataClient } from '../rule_data_client'; import { AlertExecutorOptionsWithExtraServices } from '../types'; -import { getRuleData } from './get_rule_executor_data'; +import { + CommonAlertFieldName, + CommonAlertIdFieldName, + getCommonAlertFields, +} from './get_common_alert_fields'; + +type ImplicitTechnicalFieldName = CommonAlertFieldName | CommonAlertIdFieldName; + +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, @@ -47,7 +58,7 @@ export type LifecycleAlertService< ActionGroupIds extends string = never > = (alert: { id: string; - fields: Record & Partial>; + fields: ExplicitAlertFields; }) => AlertInstance; export interface LifecycleAlertServices< @@ -129,14 +140,10 @@ export const createLifecycleExecutor = ( > ): Promise> => { const { - rule, services: { alertInstanceFactory }, state: previousState, - spaceId, } = options; - const ruleExecutorData = getRuleData(options); - const state = getOrElse( (): WrappedLifecycleRuleState => ({ wrapped: previousState as State, @@ -144,9 +151,9 @@ export const createLifecycleExecutor = ( }) )(wrappedStateRt().decode(previousState)); - const currentAlerts: Record> = {}; + const commonRuleFields = getCommonAlertFields(options); - const timestamp = options.startedAt.toISOString(); + const currentAlerts: Record = {}; const lifecycleAlertServices: LifecycleAlertServices< InstanceState, @@ -154,12 +161,8 @@ export const createLifecycleExecutor = ( ActionGroupIds > = { alertWithLifecycle: ({ id, fields }) => { - currentAlerts[id] = { - ...fields, - [ALERT_ID]: id, - [ALERT_RULE_TYPE_ID]: rule.ruleTypeId, - [ALERT_RULE_CONSUMER]: rule.consumer, - }; + currentAlerts[id] = fields; + return alertInstanceFactory(id); }, }; @@ -199,7 +202,7 @@ export const createLifecycleExecutor = ( filter: [ { term: { - [ALERT_RULE_UUID]: ruleExecutorData[ALERT_RULE_UUID], + [ALERT_RULE_UUID]: commonRuleFields[ALERT_RULE_UUID], }, }, { @@ -227,12 +230,10 @@ export const createLifecycleExecutor = ( hits.hits.forEach((hit) => { const fields = parseTechnicalFields(hit.fields); - const alertId = fields[ALERT_ID]!; + const alertId = fields[ALERT_ID]; alertsDataMap[alertId] = { + ...commonRuleFields, ...fields, - [ALERT_ID]: alertId, - [ALERT_RULE_TYPE_ID]: rule.ruleTypeId, - [ALERT_RULE_CONSUMER]: rule.consumer, }; }); } @@ -244,59 +245,28 @@ export const createLifecycleExecutor = ( logger.warn(`Could not find alert data for ${alertId}`); } - const event: Mutable = { - ...alertData, - ...ruleExecutorData, - [TIMESTAMP]: timestamp, - [EVENT_KIND]: 'signal', - [ALERT_RULE_CONSUMER]: rule.consumer, - [ALERT_ID]: alertId, - [VERSION]: ruleDataClient.kibanaVersion, - } as ParsedTechnicalFields; - const isNew = !state.trackedAlerts[alertId]; const isRecovered = !currentAlerts[alertId]; - const isActiveButNotNew = !isNew && !isRecovered; const isActive = !isRecovered; const { alertUuid, started } = state.trackedAlerts[alertId] ?? { alertUuid: v4(), - started: timestamp, + started: commonRuleFields[TIMESTAMP], + }; + const event: ParsedTechnicalFields = { + ...alertData, + ...commonRuleFields, + [ALERT_DURATION]: (options.startedAt.getTime() - new Date(started).getTime()) * 1000, + [ALERT_ID]: alertId, + [ALERT_START]: started, + [ALERT_STATUS]: isActive ? ALERT_STATUS_ACTIVE : ALERT_STATUS_RECOVERED, + [ALERT_WORKFLOW_STATUS]: alertData[ALERT_WORKFLOW_STATUS] ?? 'open', + [ALERT_UUID]: alertUuid, + [EVENT_KIND]: 'signal', + [EVENT_ACTION]: isNew ? 'open' : isActive ? 'active' : 'close', + [VERSION]: ruleDataClient.kibanaVersion, + ...(isRecovered ? { [ALERT_END]: commonRuleFields[TIMESTAMP] } : {}), }; - - event[ALERT_START] = started; - event[ALERT_UUID] = alertUuid; - event[ALERT_WORKFLOW_STATUS] = event[ALERT_WORKFLOW_STATUS] ?? 'open'; - - // not sure why typescript needs the non-null assertion here - // we already assert the value is not undefined with the ternary - // still getting an error with the ternary.. strange. - - event[SPACE_IDS] = - event[SPACE_IDS] == null - ? [spaceId] - : [spaceId, ...event[SPACE_IDS]!.filter((sid) => sid !== spaceId)]; - - if (isNew) { - event[EVENT_ACTION] = 'open'; - } - - if (isRecovered) { - event[ALERT_END] = timestamp; - event[EVENT_ACTION] = 'close'; - event[ALERT_STATUS] = 'closed'; - } - - if (isActiveButNotNew) { - event[EVENT_ACTION] = 'active'; - } - - if (isActive) { - event[ALERT_STATUS] = 'open'; - } - - event[ALERT_DURATION] = - (options.startedAt.getTime() - new Date(event[ALERT_START]!).getTime()) * 1000; return event; }); 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 index 71a0dee5deac7..2b138ae723305 100644 --- 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 @@ -6,7 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import { ALERT_DURATION, ALERT_STATUS, ALERT_UUID } from '@kbn/rule-data-utils'; +import { + ALERT_DURATION, + ALERT_STATUS, + ALERT_STATUS_ACTIVE, + ALERT_STATUS_RECOVERED, + ALERT_UUID, +} from '@kbn/rule-data-utils'; import { loggerMock } from '@kbn/logging/mocks'; import { castArray, omit, mapValues } from 'lodash'; import { RuleDataClient } from '../rule_data_client'; @@ -177,7 +183,9 @@ describe('createLifecycleRuleTypeFactory', () => { expect(evaluationDocuments.length).toBe(0); expect(alertDocuments.length).toBe(2); - expect(alertDocuments.every((doc) => doc[ALERT_STATUS] === 'open')).toBeTruthy(); + expect( + alertDocuments.every((doc) => doc[ALERT_STATUS] === ALERT_STATUS_ACTIVE) + ).toBeTruthy(); expect(alertDocuments.every((doc) => doc[ALERT_DURATION] === 0)).toBeTruthy(); @@ -198,7 +206,7 @@ describe('createLifecycleRuleTypeFactory', () => { "kibana.alert.rule.rule_type_id": "ruleTypeId", "kibana.alert.rule.uuid": "alertId", "kibana.alert.start": "2021-06-16T09:01:00.000Z", - "kibana.alert.status": "open", + "kibana.alert.status": "active", "kibana.alert.workflow_status": "open", "kibana.space_ids": Array [ "spaceId", @@ -222,7 +230,7 @@ describe('createLifecycleRuleTypeFactory', () => { "kibana.alert.rule.rule_type_id": "ruleTypeId", "kibana.alert.rule.uuid": "alertId", "kibana.alert.start": "2021-06-16T09:01:00.000Z", - "kibana.alert.status": "open", + "kibana.alert.status": "active", "kibana.alert.workflow_status": "open", "kibana.space_ids": Array [ "spaceId", @@ -284,7 +292,9 @@ describe('createLifecycleRuleTypeFactory', () => { expect(evaluationDocuments.length).toBe(0); expect(alertDocuments.length).toBe(2); - expect(alertDocuments.every((doc) => doc[ALERT_STATUS] === 'open')).toBeTruthy(); + 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(); @@ -362,10 +372,10 @@ describe('createLifecycleRuleTypeFactory', () => { ); expect(opbeansJavaAlertDoc['event.action']).toBe('active'); - expect(opbeansJavaAlertDoc[ALERT_STATUS]).toBe('open'); + expect(opbeansJavaAlertDoc[ALERT_STATUS]).toBe(ALERT_STATUS_ACTIVE); expect(opbeansNodeAlertDoc['event.action']).toBe('close'); - expect(opbeansNodeAlertDoc[ALERT_STATUS]).toBe('closed'); + expect(opbeansNodeAlertDoc[ALERT_STATUS]).toBe(ALERT_STATUS_RECOVERED); }); }); }); diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts index 30e17f1afca54..1fa51d98c8ab5 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts @@ -6,6 +6,7 @@ */ import { ALERT_ID, VERSION } from '@kbn/rule-data-utils'; +import { getCommonAlertFields } from './get_common_alert_fields'; import { CreatePersistenceRuleTypeFactory } from './persistence_types'; export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory = ({ @@ -24,13 +25,16 @@ export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory logger.debug(`Found ${numAlerts} alerts.`); if (ruleDataClient.isWriteEnabled() && numAlerts) { + const commonRuleFields = getCommonAlertFields(options); + const response = await ruleDataClient.getWriter().bulk({ body: alerts.flatMap((event) => [ { index: {} }, { - ...event.fields, [ALERT_ID]: event.id, [VERSION]: ruleDataClient.kibanaVersion, + ...commonRuleFields, + ...event.fields, }, ]), refresh, diff --git a/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts b/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts new file mode 100644 index 0000000000000..8bba639636ba6 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/get_common_alert_fields.ts @@ -0,0 +1,57 @@ +/* + * 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 { Values } from '@kbn/utility-types'; +import { AlertExecutorOptions } from '../../../alerting/server'; +import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; +import { + ALERT_ID, + ALERT_UUID, + ALERT_RULE_CATEGORY, + ALERT_RULE_CONSUMER, + ALERT_RULE_NAME, + ALERT_RULE_PRODUCER, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, + SPACE_IDS, + TAGS, + TIMESTAMP, +} from '../../common/technical_rule_data_field_names'; + +const commonAlertFieldNames = [ + ALERT_RULE_CATEGORY, + ALERT_RULE_CONSUMER, + ALERT_RULE_NAME, + ALERT_RULE_PRODUCER, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, + SPACE_IDS, + TAGS, + TIMESTAMP, +]; +export type CommonAlertFieldName = Values; + +const commonAlertIdFieldNames = [ALERT_ID, ALERT_UUID]; +export type CommonAlertIdFieldName = Values; + +export type CommonAlertFields = Pick; + +export const getCommonAlertFields = ( + options: AlertExecutorOptions +): CommonAlertFields => { + return { + [ALERT_RULE_CATEGORY]: options.rule.ruleTypeName, + [ALERT_RULE_CONSUMER]: options.rule.consumer, + [ALERT_RULE_NAME]: options.rule.name, + [ALERT_RULE_PRODUCER]: options.rule.producer, + [ALERT_RULE_TYPE_ID]: options.rule.ruleTypeId, + [ALERT_RULE_UUID]: options.alertId, + [SPACE_IDS]: [options.spaceId], + [TAGS]: options.tags, + [TIMESTAMP]: options.startedAt.toISOString(), + }; +}; diff --git a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts deleted file mode 100644 index 13f0b27e85c3b..0000000000000 --- a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts +++ /dev/null @@ -1,36 +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 { AlertExecutorOptions } from '../../../alerting/server'; -import { - ALERT_RULE_PRODUCER, - ALERT_RULE_CATEGORY, - ALERT_RULE_TYPE_ID, - ALERT_RULE_NAME, - ALERT_RULE_UUID, - TAGS, -} from '../../common/technical_rule_data_field_names'; - -export interface RuleExecutorData { - [ALERT_RULE_CATEGORY]: string; - [ALERT_RULE_TYPE_ID]: string; - [ALERT_RULE_UUID]: string; - [ALERT_RULE_NAME]: string; - [ALERT_RULE_PRODUCER]: string; - [TAGS]: string[]; -} - -export function getRuleData(options: AlertExecutorOptions) { - return { - [ALERT_RULE_TYPE_ID]: options.rule.ruleTypeId, - [ALERT_RULE_UUID]: options.alertId, - [ALERT_RULE_CATEGORY]: options.rule.ruleTypeName, - [ALERT_RULE_NAME]: options.rule.name, - [TAGS]: options.tags, - [ALERT_RULE_PRODUCER]: options.rule.producer, - }; -} diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts new file mode 100644 index 0000000000000..b74fa27879f3d --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts @@ -0,0 +1,77 @@ +/* + * 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 { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../src/core/server/mocks'; +import { + AlertExecutorOptions, + AlertInstanceContext, + AlertInstanceState, + AlertTypeParams, + AlertTypeState, +} from '../../../alerting/server'; +import { alertsMock } from '../../../alerting/server/mocks'; + +export const createDefaultAlertExecutorOptions = < + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = {}, + InstanceContext extends AlertInstanceContext = {}, + ActionGroupIds extends string = '' +>({ + alertId = 'ALERT_ID', + ruleName = 'ALERT_RULE_NAME', + params, + state, + createdAt = new Date(), + startedAt = new Date(), + updatedAt = new Date(), +}: { + alertId?: string; + ruleName?: string; + params: Params; + state: State; + createdAt?: Date; + startedAt?: Date; + updatedAt?: Date; +}): AlertExecutorOptions => ({ + alertId, + createdBy: 'CREATED_BY', + startedAt, + name: ruleName, + rule: { + updatedBy: null, + tags: [], + name: ruleName, + createdBy: null, + actions: [], + enabled: true, + consumer: 'CONSUMER', + producer: 'ALERT_PRODUCER', + schedule: { interval: '1m' }, + throttle: null, + createdAt, + updatedAt, + notifyWhen: null, + ruleTypeId: 'RULE_TYPE_ID', + ruleTypeName: 'RULE_TYPE_NAME', + }, + tags: [], + params, + spaceId: 'SPACE_ID', + services: { + alertInstanceFactory: alertsMock.createAlertServices() + .alertInstanceFactory, + savedObjectsClient: savedObjectsClientMock.create(), + scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + }, + state, + updatedBy: null, + previousStartedAt: null, + namespace: undefined, +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts index 0c533ed026901..cbc6e570e936f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts @@ -33,5 +33,8 @@ export const parseRuleExecutionLog = (input: unknown) => { /** * @deprecated RuleExecutionEvent is kept here only as a reference. It will be superseded with EventLog implementation + * + * It's marked as `Partial` because the field map is not yet appropriate for + * execution log events. */ -export type RuleExecutionEvent = ReturnType; +export type RuleExecutionEvent = Partial>; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 1a8389d450ab3..d56344b7707db 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -19,6 +19,9 @@ import { AlertAttributes } from '../../signals/types'; import { createRuleMock } from './rule'; import { listMock } from '../../../../../../lists/server/mocks'; import { RuleParams } from '../../schemas/rule_schemas'; +// this is only used in tests +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createDefaultAlertExecutorOptions } from '../../../../../../rule_registry/server/utils/rule_executor_test_utils'; export const createRuleTypeMocks = ( ruleType: string = 'query', @@ -90,10 +93,12 @@ export const createRuleTypeMocks = ( scheduleActions, executor: async ({ params }: { params: Record }) => { return alertExecutor({ + ...createDefaultAlertExecutorOptions({ + params, + alertId: v4(), + state: {}, + }), services, - params, - alertId: v4(), - startedAt: new Date(), }); }, }; diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 678aebf502869..73e0669c0e3a8 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -392,7 +392,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { "apm.transaction_error_rate", ], "kibana.alert.status": Array [ - "open", + "active", ], "kibana.alert.workflow_status": Array [ "open", @@ -461,7 +461,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { any >; - expect(recoveredAlertEvent[ALERT_STATUS]?.[0]).to.eql('closed'); + expect(recoveredAlertEvent[ALERT_STATUS]?.[0]).to.eql('recovered'); expect(recoveredAlertEvent[ALERT_DURATION]?.[0]).to.be.greaterThan(0); expect(new Date(recoveredAlertEvent[ALERT_END]?.[0]).getTime()).to.be.greaterThan(0); @@ -502,7 +502,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { "apm.transaction_error_rate", ], "kibana.alert.status": Array [ - "closed", + "recovered", ], "kibana.alert.workflow_status": Array [ "open",