diff --git a/x-pack/plugins/rule_registry/server/plugin_v2.ts b/x-pack/plugins/rule_registry/server/plugin_v2.ts new file mode 100644 index 000000000000000..adaea25ed16b524 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/plugin_v2.ts @@ -0,0 +1,84 @@ +// /* +// * 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 { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +// import { PluginSetupContract as AlertingPluginSetupContract } from '../../alerting/server'; +// import { BaseRuleFieldMap, baseRuleFieldMap } from '../common'; +// import { EventLogService, Schema, defaultIlmPolicy } from './event_log'; +// import { RuleRegistry } from './rule_registry_v2'; +// import { RuleRegistryConfig } from '.'; + +// export type RuleRegistryPluginSetupContract = RuleRegistry; + +// export class RuleRegistryPlugin implements Plugin { +// private readonly initContext: PluginInitializerContext; +// private eventLogService: EventLogService | null; + +// constructor(initContext: PluginInitializerContext) { +// this.initContext = initContext; +// this.eventLogService = null; +// } + +// public setup( +// core: CoreSetup, +// plugins: { alerting: AlertingPluginSetupContract } +// ): RuleRegistryPluginSetupContract { +// const globalConfig = this.initContext.config.legacy.get(); +// const config = this.initContext.config.get(); +// const logger = this.initContext.logger.get(); + +// this.eventLogService = new EventLogService({ +// config: { +// kibanaIndexName: globalConfig.kibana.index, +// kibanaVersion: this.initContext.env.packageInfo.version, +// isWriteEnabled: config.unsafe.write.enabled, +// }, +// dependencies: { +// clusterClient: core.getStartServices().then(([{ elasticsearch }]) => elasticsearch.client), +// logger: logger.get('event-log'), +// }, +// }); + +// const rootLogProvider = this.eventLogService.registerLog({ +// name: 'alerting', +// schema: Schema.create(baseRuleFieldMap), +// ilmPolicy: defaultIlmPolicy, +// }); + +// const alertsLogProvider = rootLogProvider.registerLog({ +// name: 'alerts', +// schema: Schema.create(baseRuleFieldMap), +// }); + +// const executionLogProvider = rootLogProvider.registerLog({ +// name: 'execlog', +// schema: Schema.create(baseRuleFieldMap), +// }); + +// const rootRegistry = new RuleRegistry({ +// alertingPlugin: plugins.alerting, +// alertsLogProvider, +// executionLogProvider, +// logger: logger.get('root'), +// writeEnabled: config.unsafe.write.enabled, +// }); + +// return rootRegistry; +// } + +// public start() { +// if (this.eventLogService) { +// this.eventLogService.start(); +// } +// } + +// public stop() { +// if (this.eventLogService) { +// this.eventLogService.stop(); +// } +// } +// } diff --git a/x-pack/plugins/rule_registry/server/rule_registry_v2/create_scoped_rule_registry_client/index.ts b/x-pack/plugins/rule_registry/server/rule_registry_v2/create_scoped_rule_registry_client/index.ts new file mode 100644 index 000000000000000..f314f99a8596ee8 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_registry_v2/create_scoped_rule_registry_client/index.ts @@ -0,0 +1,181 @@ +/* + * 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 { Either, isLeft, isRight } from 'fp-ts/lib/Either'; +import { Errors } from 'io-ts'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { Logger } from 'kibana/server'; +import { IScopedClusterClient as ScopedClusterClient } from 'src/core/server'; +import { castArray, compact } from 'lodash'; +import { ESSearchRequest } from 'typings/elasticsearch'; +import { IndexPatternsFetcher } from '../../../../../../src/plugins/data/server'; +import { TypeOfFieldMap } from '../../../common'; +import { ScopedRuleRegistryClient, EventsOf } from './types'; +import { BaseRuleFieldMap } from '../../../common'; +import { RuleRegistry } from '..'; +import { IEventLogProvider } from '../../event_log'; + +const createPathReporterError = (either: Either) => { + const error = new Error(`Failed to validate alert event`); + error.stack += '\n' + PathReporter.report(either).join('\n'); + return error; +}; + +export function createScopedRuleRegistryClient({ + ruleUuids, + scopedClusterClient, + eventLogProvider, + logger, + registry, + ruleData, +}: { + ruleUuids: string[]; + scopedClusterClient: ScopedClusterClient; + eventLogProvider: IEventLogProvider; + logger: Logger; + registry: RuleRegistry; + ruleData?: { + rule: { + id: string; + uuid: string; + category: string; + name: string; + }; + producer: string; + tags: string[]; + }; +}): ScopedRuleRegistryClient { + const fieldmapType = registry.getFieldMapType(); + + const defaults = ruleData + ? { + 'rule.uuid': ruleData.rule.uuid, + 'rule.id': ruleData.rule.id, + 'rule.name': ruleData.rule.name, + 'rule.category': ruleData.rule.category, + 'kibana.rac.producer': ruleData.producer, + tags: ruleData.tags, + } + : {}; + + const client: ScopedRuleRegistryClient = { + search: async (searchRequest) => { + const fields = [ + 'rule.id', + ...(searchRequest.body?.fields ? castArray(searchRequest.body.fields) : []), + ]; + + const eventLog = await eventLogProvider.getLog(); + const query = eventLog.getEvents().buildQuery(); + + const response = await query.search({ + ...searchRequest, + body: { + ...searchRequest.body, + query: { + bool: { + filter: [ + { terms: { 'rule.uuid': ruleUuids } }, + ...compact([searchRequest.body?.query]), + ], + }, + }, + fields, + }, + }); + + return { + body: response.body as any, + events: compact( + response.body.hits.hits.map((hit) => { + const ruleTypeId: string = hit.fields!['rule.id'][0]; + + const registryOfType = registry.getRegistryByRuleTypeId(ruleTypeId); + + if (ruleTypeId && !registryOfType) { + logger.warn( + `Could not find type ${ruleTypeId} in registry, decoding with default type` + ); + } + + const type = registryOfType?.getFieldMapType() ?? fieldmapType; + + const validation = type.decode(hit.fields); + if (isLeft(validation)) { + const error = createPathReporterError(validation); + logger.error(error); + return undefined; + } + return type.encode(validation.right); + }) + ) as EventsOf, + }; + }, + getDynamicIndexPattern: async () => { + const { logIndexBasePattern } = eventLogProvider.getIndexSpec().indexNames; + + const indexPatternsFetcher = new IndexPatternsFetcher(scopedClusterClient.asInternalUser); + const fields = await indexPatternsFetcher.getFieldsForWildcard({ + pattern: logIndexBasePattern, + }); + + return { + fields, + timeFieldName: '@timestamp', + title: logIndexBasePattern, + }; + }, + index: async (doc) => { + const validation = fieldmapType.decode({ + ...doc, + ...defaults, + }); + + if (isLeft(validation)) { + throw createPathReporterError(validation); + } + + const x = validation.right; + + const eventLog = await eventLogProvider.getLog(); + const eventLogger = eventLog.getLogger('alerts'); + eventLogger.logEvent(validation.right); + }, + bulkIndex: async (docs) => { + const validations = docs.map((doc) => { + return fieldmapType.decode({ + ...doc, + ...defaults, + }); + }); + + const validationErrors = compact( + validations.map((validation) => + isLeft(validation) ? createPathReporterError(validation) : null + ) + ); + + const validDocuments = compact( + validations.map((validation) => (isRight(validation) ? validation.right : null)) + ); + + validationErrors.forEach((error) => { + logger.error(error); + }); + + const eventLog = await eventLogProvider.getLog(); + const eventLogger = eventLog.getLogger('alerts'); + + validDocuments.forEach((doc) => { + eventLogger.logEvent(doc); + }); + }, + }; + + // @ts-expect-error: We can't use ScopedRuleRegistryClient + // when creating the client, due to #41693 which will be fixed in 4.2 + return client; +} diff --git a/x-pack/plugins/rule_registry/server/rule_registry_v2/create_scoped_rule_registry_client/types.ts b/x-pack/plugins/rule_registry/server/rule_registry_v2/create_scoped_rule_registry_client/types.ts new file mode 100644 index 000000000000000..a49d6c46228cb17 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_registry_v2/create_scoped_rule_registry_client/types.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 { FieldDescriptor } from 'src/plugins/data/server'; +import { ESSearchRequest, ESSearchResponse } from 'typings/elasticsearch'; +import { + PatternsUnionOf, + PickWithPatterns, + OutputOfFieldMap, + BaseRuleFieldMap, +} from '../../../common'; +import { Event } from '../../event_log'; + +export type PrepopulatedRuleEventFields = keyof Pick< + BaseRuleFieldMap, + 'rule.uuid' | 'rule.id' | 'rule.name' | 'rule.category' | 'kibana.rac.producer' +>; + +type FieldsOf = + | Array<{ field: PatternsUnionOf } | PatternsUnionOf> + | PatternsUnionOf; + +type Fields = Array<{ field: TPattern } | TPattern> | TPattern; + +type FieldsESSearchRequest = ESSearchRequest & { + body?: { fields: FieldsOf }; +}; + +export type EventsOf< + TFieldsESSearchRequest extends ESSearchRequest, + TFieldMap extends BaseRuleFieldMap +> = TFieldsESSearchRequest extends { body: { fields: infer TFields } } + ? TFields extends Fields + ? Array>> + : never + : never; + +export interface ScopedRuleRegistryClient { + search>( + request: TSearchRequest + ): Promise<{ + body: ESSearchResponse; + events: EventsOf; + }>; + getDynamicIndexPattern(): Promise<{ + title: string; + timeFieldName: string; + fields: FieldDescriptor[]; + }>; + + index(event: Event): Promise; + bulkIndex(events: Array>): Promise; +} diff --git a/x-pack/plugins/rule_registry/server/rule_registry_v2/index.ts b/x-pack/plugins/rule_registry/server/rule_registry_v2/index.ts new file mode 100644 index 000000000000000..1f14097f54102df --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_registry_v2/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './public_api'; +export * from './rule_registry'; diff --git a/x-pack/plugins/rule_registry/server/rule_registry_v2/public_api.ts b/x-pack/plugins/rule_registry/server/rule_registry_v2/public_api.ts new file mode 100644 index 000000000000000..1fe317bf926ce95 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_registry_v2/public_api.ts @@ -0,0 +1,144 @@ +/* + * 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 } from '@kbn/config-schema'; +import { Logger } from 'kibana/server'; + +import { ActionVariable } from '../../../alerting/common'; +import { ActionGroup, AlertServices } from '../../../alerting/server'; + +export interface IRuleRegistry { + getDependencies(): TDeps; + + createChildRegistry( + inject: (deps: TDeps) => TMoreDeps + ): IRuleRegistry; + + registerRuleType< + TParams extends RuleParams, + TState extends RuleState = {}, + TInstanceState extends RuleInstanceState = {}, + TInstanceContext extends RuleInstanceContext = {}, + TActionGroupIds extends string = never, + TActionVariable extends ActionVariable = ActionVariable + >( + ruleType: RuleType< + TDeps, + TParams, + TState, + TInstanceState, + TInstanceContext, + TActionGroupIds, + TActionVariable + > + ): void; +} + +export type Obj = Record; +export type Schema = Type; + +export interface RuleDeps extends Obj { + logger: Logger; +} + +export type RuleParams = Obj; +export type RuleState = Obj; +export type RuleInstanceState = Obj; +export type RuleInstanceContext = Obj; + +export type RuleServices< + TDeps extends RuleDeps, + TInstanceState extends RuleInstanceState, + TInstanceContext extends RuleInstanceContext, + TActionGroupIds extends string +> = TDeps & AlertServices; + +export interface RuleExecutorOptions< + TDeps extends RuleDeps, + TParams extends RuleParams, + TState extends RuleState, + TInstanceState extends RuleInstanceState, + TInstanceContext extends RuleInstanceContext, + TActionGroupIds extends string +> { + services: RuleServices; + params: TParams; + state: TState; + + startedAt: Date; + previousStartedAt: Date | null; + + rule: { + id: string; + uuid: string; + name: string; + category: string; + }; + producer: string; + + alertId: string; + spaceId: string; + namespace?: string; + name: string; + tags: string[]; + createdBy: string | null; + updatedBy: string | null; +} + +export type RuleExecutorResult = Record; + +export type RuleExecutorFunction< + TDeps extends RuleDeps, + TParams extends RuleParams, + TState extends RuleState, + TInstanceState extends RuleInstanceState, + TInstanceContext extends RuleInstanceContext, + TActionGroupIds extends string +> = ( + options: RuleExecutorOptions< + TDeps, + TParams, + TState, + TInstanceState, + TInstanceContext, + TActionGroupIds + > +) => Promise; + +export interface RuleType< + TDeps extends RuleDeps, + TParams extends RuleParams, + TState extends RuleState, + TInstanceState extends RuleInstanceState, + TInstanceContext extends RuleInstanceContext, + TActionGroupIds extends string, + TActionVariable extends ActionVariable +> { + id: string; + name: string; + actionGroups: Array>; + defaultActionGroupId: string; + producer: string; + minimumLicenseRequired: 'basic' | 'gold' | 'trial'; + + validate: { + params: Schema; + }; + + actionVariables: { + context: TActionVariable[]; + }; + + executor: RuleExecutorFunction< + TDeps, + TParams, + TState, + TInstanceState, + TInstanceContext, + TActionGroupIds + >; +} diff --git a/x-pack/plugins/rule_registry/server/rule_registry_v2/rule_registry.ts b/x-pack/plugins/rule_registry/server/rule_registry_v2/rule_registry.ts new file mode 100644 index 000000000000000..10ace9cdeccef14 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_registry_v2/rule_registry.ts @@ -0,0 +1,203 @@ +// /* +// * 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, RequestHandlerContext } from 'kibana/server'; +// import { AlertsClient } from '../../../alerting/server'; +// import { SpacesServiceStart } from '../../../spaces/server'; +// import { +// ActionVariable, +// AlertInstanceState, +// AlertTypeParams, +// AlertTypeState, +// } from '../../../alerting/common'; +// import { FieldMap, BaseRuleFieldMap } from '../../common'; +// import { PluginSetupContract as AlertingPluginSetupContract } from '../../../alerting/server'; +// import { createScopedRuleRegistryClient } from './create_scoped_rule_registry_client'; +// import { +// IRuleRegistry, +// Obj, +// RuleDeps, +// RuleInstanceContext, +// RuleInstanceState, +// RuleParams, +// RuleState, +// RuleType, +// } from './public_api'; + +// export interface RuleRegistryDependencies { +// alertingPlugin: AlertingPluginSetupContract; +// spacesPlugin?: SpacesServiceStart; +// logger: Logger; +// writeEnabled: boolean; +// } + +// export class RuleRegistry implements IRuleRegistry { +// private readonly children: Array> = []; +// private readonly types: Array> = []; + +// constructor( +// private readonly registryDeps: RuleRegistryDependencies, +// private readonly ruleDeps: TDeps +// ) {} + +// public getDependencies(): TDeps { +// return this.ruleDeps; +// } + +// public createChildRegistry( +// inject: (deps: TDeps) => TMoreDeps +// ): IRuleRegistry { +// const moreDeps = inject(this.ruleDeps); +// const childDeps = { ...this.ruleDeps, ...moreDeps }; +// const childRegistry = new RuleRegistry(this.registryDeps, childDeps); + +// this.children.push(childRegistry); + +// return childRegistry; +// } + +// public registerRuleType< +// TParams extends RuleParams, +// TState extends RuleState = {}, +// TInstanceState extends RuleInstanceState = {}, +// TInstanceContext extends RuleInstanceContext = {}, +// TActionGroupIds extends string = never, +// TActionVariable extends ActionVariable = ActionVariable +// >( +// ruleType: RuleType< +// TDeps, +// TParams, +// TState, +// TInstanceState, +// TInstanceContext, +// TActionGroupIds, +// TActionVariable +// > +// ): void { +// const logger = this.registryDeps.logger.get(ruleType.id); + +// this.types.push(ruleType); + +// this.registryDeps.alertingPlugin.registerType< +// TParams, +// TState, +// TInstanceState, +// TInstanceContext, +// TActionGroupIds +// >({ +// ...ruleType, +// executor: async (executorOptions) => { +// const { services, alertId, name, tags } = executorOptions; + +// const rule = { +// id: type.id, +// uuid: alertId, +// category: type.name, +// name, +// }; + +// const producer = type.producer; + +// return type.executor({ +// ...executorOptions, +// rule, +// producer, +// services: { +// ...services, +// logger, +// ...(this.options.writeEnabled +// ? { +// scopedRuleRegistryClient: createScopedRuleRegistryClient({ +// ruleUuids: [rule.uuid], +// scopedClusterClient: services.scopedClusterClient, +// eventLogProvider: this.eventLogProvider, +// registry: this, +// ruleData: { +// producer, +// rule, +// tags, +// }, +// logger: this.options.logger, +// }), +// } +// : {}), +// }, +// }); +// }, +// }); +// } + +// // ----------------------------------------------------------------------------------------------- + +// DELgetRuleTypeById(ruleTypeId: string) { +// return this.types.find((type) => type.id === ruleTypeId); +// } + +// DELgetRegistryByRuleTypeId(ruleTypeId: string): RuleRegistry | undefined { +// if (this.getRuleTypeById(ruleTypeId)) { +// return this; +// } + +// return this.children.find((child) => child.getRegistryByRuleTypeId(ruleTypeId)); +// } + +// DELregisterType( +// type: RuleType +// ) { +// const logger = this.options.logger.get(type.id); + +// this.types.push(type); + +// this.options.alertingPlugin.registerType< +// AlertTypeParams, +// AlertTypeState, +// AlertInstanceState, +// { [key in TActionVariable['name']]: any }, +// string +// >({ +// ...type, +// executor: async (executorOptions) => { +// const { services, alertId, name, tags } = executorOptions; + +// const rule = { +// id: type.id, +// uuid: alertId, +// category: type.name, +// name, +// }; + +// const producer = type.producer; + +// return type.executor({ +// ...executorOptions, +// rule, +// producer, +// services: { +// ...services, +// logger, +// ...(this.options.writeEnabled +// ? { +// scopedRuleRegistryClient: createScopedRuleRegistryClient({ +// ruleUuids: [rule.uuid], +// scopedClusterClient: services.scopedClusterClient, +// eventLogProvider: this.eventLogProvider, +// registry: this, +// ruleData: { +// producer, +// rule, +// tags, +// }, +// logger: this.options.logger, +// }), +// } +// : {}), +// }, +// }); +// }, +// }); +// } +// } diff --git a/x-pack/plugins/rule_registry/server/rule_registry_v2/rule_type_helpers/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/rule_registry_v2/rule_type_helpers/create_lifecycle_rule_type_factory.ts new file mode 100644 index 000000000000000..7de8a9ba1261e4f --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_registry_v2/rule_type_helpers/create_lifecycle_rule_type_factory.ts @@ -0,0 +1,235 @@ +/* + * 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 * as t from 'io-ts'; +import { isLeft } from 'fp-ts/lib/Either'; +import v4 from 'uuid/v4'; +import { Mutable } from 'utility-types'; +import { AlertInstance } from '../../../../alerting/server'; +import { ActionVariable, AlertInstanceState } from '../../../../alerting/common'; +import { RuleParams, RuleType } from '../public_api'; +import { BaseRuleFieldMap, OutputOfFieldMap } from '../../../common'; +import { PrepopulatedRuleEventFields } from '../create_scoped_rule_registry_client/types'; +import { RuleRegistry } from '..'; + +type UserDefinedAlertFields = Omit< + OutputOfFieldMap, + PrepopulatedRuleEventFields | 'kibana.rac.alert.id' | 'kibana.rac.alert.uuid' | '@timestamp' +>; + +type LifecycleAlertService< + TFieldMap extends BaseRuleFieldMap, + TActionVariable extends ActionVariable +> = (alert: { + id: string; + fields: UserDefinedAlertFields; +}) => AlertInstance; + +type CreateLifecycleRuleType = < + TRuleParams extends RuleParams, + TActionVariable extends ActionVariable +>( + type: RuleType< + TFieldMap, + TRuleParams, + TActionVariable, + { alertWithLifecycle: LifecycleAlertService } + > +) => RuleType; + +const trackedAlertStateRt = t.type({ + alertId: t.string, + alertUuid: t.string, + started: t.string, +}); + +const wrappedStateRt = t.type({ + wrapped: t.record(t.string, t.unknown), + trackedAlerts: t.record(t.string, trackedAlertStateRt), +}); + +export function createLifecycleRuleTypeFactory< + TRuleRegistry extends RuleRegistry +>(): TRuleRegistry extends RuleRegistry + ? CreateLifecycleRuleType + : never; + +export function createLifecycleRuleTypeFactory(): CreateLifecycleRuleType { + return (type) => { + return { + ...type, + executor: async (options) => { + const { + services: { scopedRuleRegistryClient, alertInstanceFactory, logger }, + state: previousState, + rule, + } = options; + + const decodedState = wrappedStateRt.decode(previousState); + + const state = isLeft(decodedState) + ? { + wrapped: previousState, + trackedAlerts: {}, + } + : decodedState.right; + + const currentAlerts: Record< + string, + UserDefinedAlertFields & { 'kibana.rac.alert.id': string } + > = {}; + + const timestamp = options.startedAt.toISOString(); + + const nextWrappedState = await type.executor({ + ...options, + state: state.wrapped, + services: { + ...options.services, + alertWithLifecycle: ({ id, fields }) => { + currentAlerts[id] = { + ...fields, + 'kibana.rac.alert.id': id, + }; + return alertInstanceFactory(id); + }, + }, + }); + + const currentAlertIds = Object.keys(currentAlerts); + const trackedAlertIds = Object.keys(state.trackedAlerts); + const newAlertIds = currentAlertIds.filter((alertId) => !trackedAlertIds.includes(alertId)); + + const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; + + const trackedAlertStatesOfRecovered = Object.values(state.trackedAlerts).filter( + (trackedAlertState) => !currentAlerts[trackedAlertState.alertId] + ); + + logger.debug( + `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStatesOfRecovered.length} recovered)` + ); + + const alertsDataMap: Record> = { + ...currentAlerts, + }; + + if (scopedRuleRegistryClient && trackedAlertStatesOfRecovered.length) { + const { events } = await scopedRuleRegistryClient.search({ + body: { + query: { + bool: { + filter: [ + { + term: { + 'rule.uuid': rule.uuid, + }, + }, + { + terms: { + 'kibana.rac.alert.uuid': trackedAlertStatesOfRecovered.map( + (trackedAlertState) => trackedAlertState.alertUuid + ), + }, + }, + ], + }, + }, + size: trackedAlertStatesOfRecovered.length, + collapse: { + field: 'kibana.rac.alert.uuid', + }, + _source: false, + fields: ['*'], + sort: { + '@timestamp': 'desc' as const, + }, + }, + }); + + events.forEach((event) => { + const alertId = event['kibana.rac.alert.id']!; + alertsDataMap[alertId] = event; + }); + } + + const eventsToIndex: Array> = allAlertIds.map( + (alertId) => { + const alertData = alertsDataMap[alertId]; + + if (!alertData) { + logger.warn(`Could not find alert data for ${alertId}`); + } + + const event: Mutable> = { + ...alertData, + '@timestamp': timestamp, + 'event.kind': 'state', + 'kibana.rac.alert.id': alertId, + }; + + 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, + }; + + event['kibana.rac.alert.start'] = started; + event['kibana.rac.alert.uuid'] = alertUuid; + + if (isNew) { + event['event.action'] = 'open'; + } + + if (isRecovered) { + event['kibana.rac.alert.end'] = timestamp; + event['event.action'] = 'close'; + event['kibana.rac.alert.status'] = 'closed'; + } + + if (isActiveButNotNew) { + event['event.action'] = 'active'; + } + + if (isActive) { + event['kibana.rac.alert.status'] = 'open'; + } + + event['kibana.rac.alert.duration.us'] = + (options.startedAt.getTime() - new Date(event['kibana.rac.alert.start']!).getTime()) * + 1000; + + return event; + } + ); + + if (eventsToIndex.length && scopedRuleRegistryClient) { + await scopedRuleRegistryClient.bulkIndex(eventsToIndex); + } + + const nextTrackedAlerts = Object.fromEntries( + eventsToIndex + .filter((event) => event['kibana.rac.alert.status'] !== 'closed') + .map((event) => { + const alertId = event['kibana.rac.alert.id']!; + const alertUuid = event['kibana.rac.alert.uuid']!; + const started = new Date(event['kibana.rac.alert.start']!).toISOString(); + return [alertId, { alertId, alertUuid, started }]; + }) + ); + + return { + wrapped: nextWrappedState, + trackedAlerts: nextTrackedAlerts, + }; + }, + }; + }; +}