diff --git a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts index 1366503ea14284..679f33707b5b53 100644 --- a/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/lib/alerts/test_utils/index.ts @@ -70,8 +70,14 @@ export const createRuleTypeMocks = () => { executor: async ({ params }: { params: Record }) => { return alertExecutor({ services, - rule: { consumer: APM_SERVER_FEATURE_ID }, params, + rule: { + consumer: APM_SERVER_FEATURE_ID, + name: 'name', + producer: 'producer', + ruleTypeId: 'ruleTypeId', + ruleTypeName: 'ruleTypeName', + }, startedAt: new Date(), }); }, diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/index.ts new file mode 100644 index 00000000000000..3f4cbc82c405cd --- /dev/null +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/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 './rule_data'; +export * from './types'; diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts new file mode 100644 index 00000000000000..dd607392897562 --- /dev/null +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/rule_data.ts @@ -0,0 +1,19 @@ +/* + * 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 { jsonRt } from '@kbn/io-ts-utils/target/json_rt'; +import * as rt from 'io-ts'; +import { alertParamsRT as logThresholdAlertParamsRT } from './types'; + +export const serializedParamsKey = 'serialized_params'; + +export const logThresholdRuleDataNamespace = 'log_threshold_rule'; +export const logThresholdRuleDataSerializedParamsKey = `${logThresholdRuleDataNamespace}.${serializedParamsKey}` as const; + +export const logThresholdRuleDataRT = rt.type({ + [logThresholdRuleDataSerializedParamsKey]: rt.array(jsonRt.pipe(logThresholdAlertParamsRT)), +}); diff --git a/x-pack/plugins/infra/common/constants.ts b/x-pack/plugins/infra/common/constants.ts index 9362293fce82f5..1c3aa550f2f629 100644 --- a/x-pack/plugins/infra/common/constants.ts +++ b/x-pack/plugins/infra/common/constants.ts @@ -11,3 +11,8 @@ export const LOGS_INDEX_PATTERN = 'logs-*,filebeat-*,kibana_sample_data_logs*'; export const TIMESTAMP_FIELD = '@timestamp'; export const METRICS_APP = 'metrics'; export const LOGS_APP = 'logs'; + +export const METRICS_FEATURE_ID = 'infrastructure'; +export const LOGS_FEATURE_ID = 'logs'; + +export type InfraFeatureId = typeof METRICS_FEATURE_ID | typeof LOGS_FEATURE_ID; diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index ec1b11c90f7a31..981036114282ed 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -12,7 +12,8 @@ "visTypeTimeseries", "alerting", "triggersActionsUi", - "observability" + "observability", + "ruleRegistry" ], "optionalPlugins": ["ml", "home", "embeddable"], "server": true, diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/index.ts b/x-pack/plugins/infra/public/alerting/log_threshold/index.ts index 5bd64de2f3ac26..0f2746b4469278 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/index.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { getAlertType } from './log_threshold_alert_type'; +export * from './log_threshold_alert_type'; export { AlertDropdown } from './components/alert_dropdown'; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts index 2c8a6a7ea286a9..44097fd005cc7b 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts @@ -7,14 +7,15 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, PartialAlertParams, } from '../../../common/alerting/logs/log_threshold/types'; +import { formatReason } from './rule_data_formatters'; import { validateExpression } from './validation'; -export function getAlertType(): AlertTypeModel { +export function createLogThresholdAlertType(): ObservabilityRuleTypeModel { return { id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { @@ -33,5 +34,6 @@ export function getAlertType(): AlertTypeModel { } ), requiresAppContext: false, + format: formatReason, }; } diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/rule_data_formatters.ts b/x-pack/plugins/infra/public/alerting/log_threshold/rule_data_formatters.ts new file mode 100644 index 00000000000000..6ca081ffbc5ef6 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/log_threshold/rule_data_formatters.ts @@ -0,0 +1,87 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_ID, + ALERT_START, +} from '@kbn/rule-data-utils'; +import { modifyUrl } from '@kbn/std'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/function'; +import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; +import { + ComparatorToi18nMap, + logThresholdRuleDataRT, + logThresholdRuleDataSerializedParamsKey, + ratioAlertParamsRT, +} from '../../../common/alerting/logs/log_threshold'; + +export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { + const reason = pipe( + logThresholdRuleDataRT.decode(fields), + fold( + () => + i18n.translate('xpack.infra.logs.alerting.threshold.unknownReasonDescription', { + defaultMessage: 'unknown reason', + }), + (logThresholdRuleData) => { + const params = logThresholdRuleData[logThresholdRuleDataSerializedParamsKey][0]; + + const actualCount = fields[ALERT_EVALUATION_VALUE]; + const groupName = fields[ALERT_ID]; + const isGrouped = (params.groupBy?.length ?? 0) > 0; + const isRatio = ratioAlertParamsRT.is(params); + const thresholdCount = fields[ALERT_EVALUATION_THRESHOLD]; + const translatedComparator = ComparatorToi18nMap[params.count.comparator]; + + if (isRatio) { + return i18n.translate('xpack.infra.logs.alerting.threshold.ratioAlertReasonDescription', { + defaultMessage: + '{isGrouped, select, true{{groupName}: } false{}}The log entries ratio is {actualCount} ({translatedComparator} {thresholdCount}).', + values: { + actualCount, + translatedComparator, + groupName, + isGrouped, + thresholdCount, + }, + }); + } else { + return i18n.translate('xpack.infra.logs.alerting.threshold.countAlertReasonDescription', { + defaultMessage: + '{isGrouped, select, true{{groupName}: } false{}}{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {thresholdCount}) match the configured conditions.', + values: { + actualCount, + translatedComparator, + groupName, + isGrouped, + thresholdCount, + }, + }); + } + } + ) + ); + + const alertStartDate = fields[ALERT_START]; + const timestamp = alertStartDate != null ? new Date(alertStartDate).valueOf() : null; + const link = modifyUrl('/app/logs/link-to/default/logs', ({ query, ...otherUrlParts }) => ({ + ...otherUrlParts, + query: { + ...query, + ...(timestamp != null ? { time: `${timestamp}` } : {}), + }, + })); + + return { + reason, + link, // TODO: refactor to URL generators + }; +}; diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index fd599aed5f890f..d5951d9ec9915c 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -10,21 +10,21 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public'; import { from } from 'rxjs'; import { map } from 'rxjs/operators'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; -import { createMetricThresholdAlertType } from './alerting/metric_threshold'; import { createInventoryMetricAlertType } from './alerting/inventory'; -import { getAlertType as getLogsAlertType } from './alerting/log_threshold'; +import { createLogThresholdAlertType } from './alerting/log_threshold'; +import { createMetricThresholdAlertType } from './alerting/metric_threshold'; +import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/log_stream_embeddable'; +import { LogStreamEmbeddableFactoryDefinition } from './components/log_stream/log_stream_embeddable_factory'; +import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers'; import { registerFeatures } from './register_feature'; import { - InfraClientSetupDeps, - InfraClientStartDeps, InfraClientCoreSetup, InfraClientCoreStart, InfraClientPluginClass, + InfraClientSetupDeps, + InfraClientStartDeps, } from './types'; import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers'; -import { createMetricsHasData, createMetricsFetchData } from './metrics_overview_fetchers'; -import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/log_stream_embeddable'; -import { LogStreamEmbeddableFactoryDefinition } from './components/log_stream/log_stream_embeddable_factory'; export class Plugin implements InfraClientPluginClass { constructor(_context: PluginInitializerContext) {} @@ -35,7 +35,9 @@ export class Plugin implements InfraClientPluginClass { } pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createInventoryMetricAlertType()); - pluginsSetup.triggersActionsUi.alertTypeRegistry.register(getLogsAlertType()); + pluginsSetup.observability.observabilityRuleTypeRegistry.register( + createLogThresholdAlertType() + ); pluginsSetup.triggersActionsUi.alertTypeRegistry.register(createMetricThresholdAlertType()); pluginsSetup.observability.dashboard.register({ diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 91f82e82b33cdf..fe0570c4950f82 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -10,9 +10,10 @@ import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/log_th import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; export const METRICS_FEATURE = { - id: 'infrastructure', + id: METRICS_FEATURE_ID, name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { defaultMessage: 'Metrics', }), @@ -71,7 +72,7 @@ export const METRICS_FEATURE = { }; export const LOGS_FEATURE = { - id: 'logs', + id: LOGS_FEATURE_ID, name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { defaultMessage: 'Logs', }), diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 2f816ec390cbd5..3d60cdf1c8f432 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -20,6 +20,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../pl import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { PluginSetupContract as AlertingPluginContract } from '../../../../../alerting/server'; import { MlPluginSetup } from '../../../../../ml/server'; +import { RuleRegistryPluginSetupContract } from '../../../../../rule_registry/server'; export interface InfraServerPluginSetupDeps { data: DataPluginSetup; @@ -29,6 +30,7 @@ export interface InfraServerPluginSetupDeps { visTypeTimeseries: VisTypeTimeseriesSetup; features: FeaturesPluginSetup; alerting: AlertingPluginContract; + ruleRegistry: RuleRegistryPluginSetupContract; ml?: MlPluginSetup; } diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index f9d0b5575abfc6..241931e610af08 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -5,59 +5,64 @@ * 2.0. */ +import { estypes } from '@elastic/elasticsearch'; import { i18n } from '@kbn/i18n'; +import { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE } from '@kbn/rule-data-utils'; import { ElasticsearchClient } from 'kibana/server'; -import { estypes } from '@elastic/elasticsearch'; import { - AlertExecutorOptions, - AlertServices, + ActionGroup, + ActionGroupIdsOf, AlertInstance, - AlertTypeParams, - AlertTypeState, AlertInstanceContext, AlertInstanceState, - ActionGroup, - ActionGroupIdsOf, + AlertTypeState, } from '../../../../../alerting/server'; import { + logThresholdRuleDataRT, + logThresholdRuleDataSerializedParamsKey, +} from '../../../../common/alerting/logs/log_threshold'; +import { + AlertParams, + alertParamsRT, AlertStates, Comparator, - AlertParams, + CountAlertParams, + CountCriteria, Criterion, - GroupedSearchQueryResponseRT, - UngroupedSearchQueryResponseRT, - UngroupedSearchQueryResponse, + getDenominator, + getNumerator, GroupedSearchQueryResponse, - alertParamsRT, - isRatioAlertParams, + GroupedSearchQueryResponseRT, hasGroupBy, - getNumerator, - getDenominator, - CountCriteria, - CountAlertParams, - RatioAlertParams, - isOptimizedGroupedSearchQueryResponse, isOptimizableGroupedThreshold, + isOptimizedGroupedSearchQueryResponse, + isRatioAlertParams, + RatioAlertParams, + UngroupedSearchQueryResponse, + UngroupedSearchQueryResponseRT, } from '../../../../common/alerting/logs/log_threshold/types'; -import { InfraBackendLibs } from '../../infra_types'; -import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { resolveLogSourceConfiguration } from '../../../../common/log_sources'; import { decodeOrThrow } from '../../../../common/runtime_types'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { InfraBackendLibs } from '../../infra_types'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; -import { resolveLogSourceConfiguration } from '../../../../common/log_sources'; -type LogThresholdActionGroups = ActionGroupIdsOf; -type LogThresholdAlertServices = AlertServices< - AlertInstanceState, - AlertInstanceContext, - LogThresholdActionGroups ->; -type LogThresholdAlertExecutorOptions = AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, +export type LogThresholdActionGroups = ActionGroupIdsOf; +export type LogThresholdAlertTypeParams = AlertParams; +export type LogThresholdAlertTypeState = AlertTypeState; // no specific state used +export type LogThresholdAlertInstanceState = AlertInstanceState; // no specific state used +export type LogThresholdAlertInstanceContext = AlertInstanceContext; // no specific instance context used + +type LogThresholdAlertInstance = AlertInstance< + LogThresholdAlertInstanceState, + LogThresholdAlertInstanceContext, LogThresholdActionGroups >; +type LogThresholdAlertInstanceFactory = ( + id: string, + threshold: number, + value: number +) => LogThresholdAlertInstance; const COMPOSITE_GROUP_SIZE = 2000; @@ -75,9 +80,26 @@ const checkValueAgainstComparatorMap: { // With forks for group_by vs ungrouped, and ratio vs non-ratio. export const createLogThresholdExecutor = (libs: InfraBackendLibs) => - async function ({ services, params }: LogThresholdAlertExecutorOptions) { - const { alertInstanceFactory, savedObjectsClient, scopedClusterClient } = services; + libs.logsRules.createLifecycleRuleExecutor< + LogThresholdAlertTypeParams, + LogThresholdAlertTypeState, + LogThresholdAlertInstanceState, + LogThresholdAlertInstanceContext, + LogThresholdActionGroups + >(async ({ services, params }) => { + const { alertWithLifecycle, savedObjectsClient, scopedClusterClient } = services; const { sources } = libs; + const alertInstanceFactory: LogThresholdAlertInstanceFactory = (id, threshold, value) => + alertWithLifecycle({ + id, + fields: { + [ALERT_EVALUATION_THRESHOLD]: threshold, + [ALERT_EVALUATION_VALUE]: value, + ...logThresholdRuleDataRT.encode({ + [logThresholdRuleDataSerializedParamsKey]: [params], + }), + }, + }); const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); const { indices, timestampField, runtimeMappings } = await resolveLogSourceConfiguration( @@ -113,7 +135,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => } catch (e) { throw new Error(e); } - }; + }); async function executeAlert( alertParams: CountAlertParams, @@ -121,7 +143,7 @@ async function executeAlert( indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields, esClient: ElasticsearchClient, - alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory'] + alertInstanceFactory: LogThresholdAlertInstanceFactory ) { const query = getESQuery(alertParams, timestampField, indexPattern, runtimeMappings); @@ -152,7 +174,7 @@ async function executeRatioAlert( indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields, esClient: ElasticsearchClient, - alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory'] + alertInstanceFactory: LogThresholdAlertInstanceFactory ) { // Ratio alert params are separated out into two standard sets of alert params const numeratorParams: AlertParams = { @@ -214,14 +236,14 @@ const getESQuery = ( export const processUngroupedResults = ( results: UngroupedSearchQueryResponse, params: CountAlertParams, - alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertInstanceFactory, alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; const documentCount = results.hits.total.value; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY, count.value, documentCount); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -240,7 +262,7 @@ export const processUngroupedRatioResults = ( numeratorResults: UngroupedSearchQueryResponse, denominatorResults: UngroupedSearchQueryResponse, params: RatioAlertParams, - alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertInstanceFactory, alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -250,7 +272,7 @@ export const processUngroupedRatioResults = ( const ratio = getRatio(numeratorCount, denominatorCount); if (ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value)) { - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY, count.value, ratio); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -308,7 +330,7 @@ const getReducedGroupByResults = ( export const processGroupByResults = ( results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: CountAlertParams, - alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertInstanceFactory, alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -319,7 +341,7 @@ export const processGroupByResults = ( const documentCount = group.documentCount; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - const alertInstance = alertInstanceFactory(group.name); + const alertInstance = alertInstanceFactory(group.name, count.value, documentCount); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, @@ -339,7 +361,7 @@ export const processGroupByRatioResults = ( numeratorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], denominatorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: RatioAlertParams, - alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'], + alertInstanceFactory: LogThresholdAlertInstanceFactory, alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -360,7 +382,7 @@ export const processGroupByRatioResults = ( ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value) ) { - const alertInstance = alertInstanceFactory(numeratorGroup.name); + const alertInstance = alertInstanceFactory(numeratorGroup.name, count.value, ratio); alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index 62d92d0487ff74..3d0bac3dd2bf5e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -6,14 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - PluginSetupContract, - AlertTypeParams, - AlertTypeState, - AlertInstanceContext, - AlertInstanceState, - ActionGroupIdsOf, -} from '../../../../../alerting/server'; +import { PluginSetupContract } from '../../../../../alerting/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, @@ -88,13 +81,7 @@ export async function registerLogThresholdAlertType( ); } - alertingPlugin.registerType< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - ActionGroupIdsOf - >({ + alertingPlugin.registerType({ id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.logs.alertName', { defaultMessage: 'Log threshold', diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index 0c57ff2e05847e..332a2e499977bb 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -5,15 +5,16 @@ * 2.0. */ +import { handleEsError } from '../../../../../src/plugins/es_ui_shared/server'; +import { InfraConfig } from '../plugin'; +import { GetLogQueryFields } from '../services/log_queries/get_log_query_fields'; +import { RulesServiceSetup } from '../services/rules'; +import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; -import { InfraConfig } from '../plugin'; -import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; -import { GetLogQueryFields } from '../services/log_queries/get_log_query_fields'; -import { handleEsError } from '../../../../../src/plugins/es_ui_shared/server'; export interface InfraDomainLibs { fields: InfraFieldsDomain; @@ -28,4 +29,6 @@ export interface InfraBackendLibs extends InfraDomainLibs { sourceStatus: InfraSourceStatus; getLogQueryFields: GetLogQueryFields; handleEsError: typeof handleEsError; + logsRules: RulesServiceSetup; + metricsRules: RulesServiceSetup; } diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 7c5666049bd60c..de445affc178e0 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -8,7 +8,9 @@ import { Server } from '@hapi/hapi'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { Logger } from '@kbn/logging'; import { CoreSetup, PluginInitializerContext, Plugin } from 'src/core/server'; +import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; @@ -32,6 +34,7 @@ import { InfraPluginRequestHandlerContext } from './types'; import { UsageCollector } from './usage/usage_collector'; import { createGetLogQueryFields } from './services/log_queries/get_log_query_fields'; import { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +import { RulesService } from './services/rules'; export const config = { schema: schema.object({ @@ -82,9 +85,25 @@ export interface InfraPluginSetup { export class InfraServerPlugin implements Plugin { public config: InfraConfig; public libs: InfraBackendLibs | undefined; + public logger: Logger; + + private logsRules: RulesService; + private metricsRules: RulesService; constructor(context: PluginInitializerContext) { this.config = context.config.get(); + this.logger = context.logger.get(); + + this.logsRules = new RulesService( + LOGS_FEATURE_ID, + 'observability.logs', + this.logger.get('logsRules') + ); + this.metricsRules = new RulesService( + METRICS_FEATURE_ID, + 'observability.metrics', + this.logger.get('metricsRules') + ); } setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { @@ -126,6 +145,8 @@ export class InfraServerPlugin implements Plugin { ...domainLibs, getLogQueryFields: createGetLogQueryFields(sources, framework), handleEsError, + logsRules: this.logsRules.setup(core, plugins), + metricsRules: this.metricsRules.setup(core, plugins), }; plugins.features.registerKibanaFeature(METRICS_FEATURE); diff --git a/x-pack/plugins/infra/server/services/rules/index.ts b/x-pack/plugins/infra/server/services/rules/index.ts new file mode 100644 index 00000000000000..eaa3d0da493e5c --- /dev/null +++ b/x-pack/plugins/infra/server/services/rules/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 './rules_service'; +export * from './types'; diff --git a/x-pack/plugins/infra/server/services/rules/rule_data_client.ts b/x-pack/plugins/infra/server/services/rules/rule_data_client.ts new file mode 100644 index 00000000000000..d693be40f10d0d --- /dev/null +++ b/x-pack/plugins/infra/server/services/rules/rule_data_client.ts @@ -0,0 +1,87 @@ +/* + * 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 { once } from 'lodash'; +import { CoreSetup, Logger } from 'src/core/server'; +import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../../../rule_registry/common/assets'; +import { RuleRegistryPluginSetupContract } from '../../../../rule_registry/server'; +import { logThresholdRuleDataNamespace } from '../../../common/alerting/logs/log_threshold'; +import type { InfraFeatureId } from '../../../common/constants'; +import { RuleRegistrationContext, RulesServiceStartDeps } from './types'; + +export const createRuleDataClient = ({ + ownerFeatureId, + registrationContext, + getStartServices, + logger, + ruleDataService, +}: { + ownerFeatureId: InfraFeatureId; + registrationContext: RuleRegistrationContext; + getStartServices: CoreSetup['getStartServices']; + logger: Logger; + ruleDataService: RuleRegistryPluginSetupContract['ruleDataService']; +}) => { + const initializeRuleDataTemplates = once(async () => { + const componentTemplateName = ruleDataService.getFullAssetName( + `${registrationContext}-mappings` + ); + + const indexNamePattern = ruleDataService.getFullAssetName(`${registrationContext}*`); + + if (!ruleDataService.isWriteEnabled()) { + return; + } + + await ruleDataService.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + properties: { + [logThresholdRuleDataNamespace]: { + properties: { + serialized_params: { + type: 'keyword', + index: false, + }, + }, + }, + }, + }, + }, + }, + }); + + await ruleDataService.createOrUpdateIndexTemplate({ + name: ruleDataService.getFullAssetName(registrationContext), + body: { + index_patterns: [indexNamePattern], + composed_of: [ + ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + componentTemplateName, + ], + }, + }); + + await ruleDataService.updateIndexMappingsMatchingPattern(indexNamePattern); + }); + + // initialize eagerly + const initializeRuleDataTemplatesPromise = initializeRuleDataTemplates().catch((err) => { + logger.error(err); + }); + + return ruleDataService.getRuleDataClient( + ownerFeatureId, + ruleDataService.getFullAssetName(registrationContext), + () => initializeRuleDataTemplatesPromise + ); +}; diff --git a/x-pack/plugins/infra/server/services/rules/rules_service.ts b/x-pack/plugins/infra/server/services/rules/rules_service.ts new file mode 100644 index 00000000000000..9341fc59d75b83 --- /dev/null +++ b/x-pack/plugins/infra/server/services/rules/rules_service.ts @@ -0,0 +1,50 @@ +/* + * 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 { CoreSetup, Logger } from 'src/core/server'; +import { createLifecycleExecutor } from '../../../../rule_registry/server'; +import { InfraFeatureId } from '../../../common/constants'; +import { createRuleDataClient } from './rule_data_client'; +import { + RuleRegistrationContext, + RulesServiceSetup, + RulesServiceSetupDeps, + RulesServiceStart, + RulesServiceStartDeps, +} from './types'; + +export class RulesService { + constructor( + public readonly ownerFeatureId: InfraFeatureId, + public readonly registrationContext: RuleRegistrationContext, + private readonly logger: Logger + ) {} + + public setup( + core: CoreSetup, + setupDeps: RulesServiceSetupDeps + ): RulesServiceSetup { + const ruleDataClient = createRuleDataClient({ + getStartServices: core.getStartServices, + logger: this.logger, + ownerFeatureId: this.ownerFeatureId, + registrationContext: this.registrationContext, + ruleDataService: setupDeps.ruleRegistry.ruleDataService, + }); + + const createLifecycleRuleExecutor = createLifecycleExecutor(this.logger, ruleDataClient); + + return { + createLifecycleRuleExecutor, + ruleDataClient, + }; + } + + public start(_startDeps: RulesServiceStartDeps): RulesServiceStart { + return {}; + } +} diff --git a/x-pack/plugins/infra/server/services/rules/types.ts b/x-pack/plugins/infra/server/services/rules/types.ts new file mode 100644 index 00000000000000..b67b79ee5d3c24 --- /dev/null +++ b/x-pack/plugins/infra/server/services/rules/types.ts @@ -0,0 +1,33 @@ +/* + * 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 { PluginSetupContract as AlertingPluginSetup } from '../../../../alerting/server'; +import { + createLifecycleExecutor, + RuleDataClient, + RuleRegistryPluginSetupContract, +} from '../../../../rule_registry/server'; + +type LifecycleRuleExecutorCreator = ReturnType; + +export interface RulesServiceSetupDeps { + alerting: AlertingPluginSetup; + ruleRegistry: RuleRegistryPluginSetupContract; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RulesServiceStartDeps {} + +export interface RulesServiceSetup { + createLifecycleRuleExecutor: LifecycleRuleExecutorCreator; + ruleDataClient: RuleDataClient; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface RulesServiceStart {} + +export type RuleRegistrationContext = 'observability.logs' | 'observability.metrics'; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 0561eab08fb452..6bafe465fd024d 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -68,5 +68,9 @@ export { createExploratoryViewUrl } from './components/shared/exploratory_view/c export { FilterValueLabel } from './components/shared/filter_value_label/filter_value_label'; export type { SeriesUrl } from './components/shared/exploratory_view/types'; -export type { ObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry'; +export type { + ObservabilityRuleTypeFormatter, + ObservabilityRuleTypeModel, + ObservabilityRuleTypeRegistry, +} from './rules/create_observability_rule_type_registry'; export { createObservabilityRuleTypeRegistryMock } from './rules/observability_rule_type_registry_mock'; diff --git a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts index 35f2dc18c2f226..d6f8c083598888 100644 --- a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts +++ b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts @@ -5,19 +5,29 @@ * 2.0. */ -import { AlertTypeModel, AlertTypeRegistryContract } from '../../../triggers_actions_ui/public'; +import { + AlertTypeModel, + AlertTypeParams, + AlertTypeRegistryContract, +} from '../../../triggers_actions_ui/public'; import { ParsedTechnicalFields } from '../../../rule_registry/common/parse_technical_fields'; import { AsDuration, AsPercent } from '../../common/utils/formatters'; -export type Formatter = (options: { +export type ObservabilityRuleTypeFormatter = (options: { fields: ParsedTechnicalFields & Record; formatters: { asDuration: AsDuration; asPercent: AsPercent }; }) => { reason: string; link: string }; +export interface ObservabilityRuleTypeModel + extends AlertTypeModel { + format: ObservabilityRuleTypeFormatter; +} + export function createObservabilityRuleTypeRegistry(alertTypeRegistry: AlertTypeRegistryContract) { - const formatters: Array<{ typeId: string; fn: Formatter }> = []; + const formatters: Array<{ typeId: string; fn: ObservabilityRuleTypeFormatter }> = []; + return { - register: (type: AlertTypeModel & { format: Formatter }) => { + register: (type: ObservabilityRuleTypeModel) => { const { format, ...rest } = type; formatters.push({ typeId: type.id, fn: format }); alertTypeRegistry.register(rest); diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index b6fd6b9a605c0a..19ea85b056bedf 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -15,6 +15,11 @@ export { RuleDataClient } from './rule_data_client'; export { IRuleDataClient } from './rule_data_client/types'; export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data'; export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; +export { + LifecycleRuleExecutor, + LifecycleAlertServices, + createLifecycleExecutor, +} from './utils/create_lifecycle_executor'; export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory'; export const plugin = (initContext: PluginInitializerContext) => diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index f8bd1940b10a8e..051789b1896bbe 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -12,7 +12,7 @@ import { AlertTypeParams, AlertTypeState, } from '../../alerting/common'; -import { AlertType } from '../../alerting/server'; +import { AlertExecutorOptions, AlertServices, AlertType } from '../../alerting/server'; import { AlertsClient } from './alert_data_client/alerts_client'; type SimpleAlertType< @@ -41,6 +41,20 @@ export type AlertTypeWithExecutor< executor: AlertTypeExecutor; }; +export type AlertExecutorOptionsWithExtraServices< + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never, + TExtraServices extends {} = never +> = Omit< + AlertExecutorOptions, + 'services' +> & { + services: AlertServices & TExtraServices; +}; + /** * @public */ 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 new file mode 100644 index 00000000000000..06c2cc8ff005db --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -0,0 +1,330 @@ +/* + * 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 { getOrElse } from 'fp-ts/lib/Either'; +import * as rt from 'io-ts'; +import { Mutable } from 'utility-types'; +import { v4 } from 'uuid'; +import { + AlertExecutorOptions, + AlertInstance, + AlertInstanceContext, + AlertInstanceState, + AlertTypeParams, + AlertTypeState, +} from '../../../alerting/server'; +import { ParsedTechnicalFields, parseTechnicalFields } from '../../common/parse_technical_fields'; +import { + ALERT_DURATION, + ALERT_END, + ALERT_ID, + ALERT_START, + ALERT_STATUS, + ALERT_UUID, + EVENT_ACTION, + EVENT_KIND, + OWNER, + RULE_UUID, + TIMESTAMP, +} from '../../common/technical_rule_data_field_names'; +import { RuleDataClient } from '../rule_data_client'; +import { AlertExecutorOptionsWithExtraServices } from '../types'; +import { getRuleData } from './get_rule_executor_data'; + +type LifecycleAlertService< + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +> = (alert: { + id: string; + fields: Record; +}) => AlertInstance; + +export interface LifecycleAlertServices< + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +> { + alertWithLifecycle: LifecycleAlertService; +} + +export type LifecycleRuleExecutor< + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +> = ( + options: AlertExecutorOptionsWithExtraServices< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds, + LifecycleAlertServices + > +) => Promise; + +const trackedAlertStateRt = rt.type({ + alertId: rt.string, + alertUuid: rt.string, + started: rt.string, +}); + +export type TrackedLifecycleAlertState = rt.TypeOf; + +const alertTypeStateRt = () => + rt.record(rt.string, rt.unknown) as rt.Type; + +const wrappedStateRt = () => + rt.type({ + wrapped: alertTypeStateRt(), + trackedAlerts: rt.record(rt.string, trackedAlertStateRt), + }); + +/** + * This is redefined instead of derived from above `wrappedStateRt` because + * there's no easy way to instantiate generic values such as the runtime type + * factory function. + */ +export type WrappedLifecycleRuleState = AlertTypeState & { + wrapped: State | void; + trackedAlerts: Record; +}; + +export const createLifecycleExecutor = (logger: Logger, ruleDataClient: RuleDataClient) => < + Params extends AlertTypeParams = never, + State extends AlertTypeState = never, + InstanceState extends AlertInstanceState = never, + InstanceContext extends AlertInstanceContext = never, + ActionGroupIds extends string = never +>( + wrappedExecutor: LifecycleRuleExecutor< + Params, + State, + InstanceState, + InstanceContext, + ActionGroupIds + > +) => async ( + options: AlertExecutorOptions< + Params, + WrappedLifecycleRuleState, + InstanceState, + InstanceContext, + ActionGroupIds + > +): Promise> => { + const { + rule, + services: { alertInstanceFactory }, + state: previousState, + } = options; + + const ruleExecutorData = getRuleData(options); + + const state = getOrElse( + (): WrappedLifecycleRuleState => ({ + wrapped: previousState as State, + trackedAlerts: {}, + }) + )(wrappedStateRt().decode(previousState)); + + const currentAlerts: Record = {}; + + const timestamp = options.startedAt.toISOString(); + + const lifecycleAlertServices: LifecycleAlertServices< + InstanceState, + InstanceContext, + ActionGroupIds + > = { + alertWithLifecycle: ({ id, fields }) => { + currentAlerts[id] = { + ...fields, + [ALERT_ID]: id, + }; + return alertInstanceFactory(id); + }, + }; + + const nextWrappedState = 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 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< + string, + { + [ALERT_ID]: string; + } + > = { + ...currentAlerts, + }; + + if (trackedAlertStatesOfRecovered.length) { + const { hits } = await ruleDataClient.getReader().search({ + body: { + query: { + bool: { + filter: [ + { + term: { + [RULE_UUID]: ruleExecutorData[RULE_UUID], + }, + }, + { + terms: { + [ALERT_UUID]: trackedAlertStatesOfRecovered.map( + (trackedAlertState) => trackedAlertState.alertUuid + ), + }, + }, + ], + }, + }, + size: trackedAlertStatesOfRecovered.length, + collapse: { + field: ALERT_UUID, + }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], + sort: { + [TIMESTAMP]: 'desc' as const, + }, + }, + allow_no_indices: true, + }); + + hits.hits.forEach((hit) => { + const fields = parseTechnicalFields(hit.fields); + const alertId = fields[ALERT_ID]!; + alertsDataMap[alertId] = { + ...fields, + [ALERT_ID]: alertId, + }; + }); + } + + const eventsToIndex = allAlertIds.map((alertId) => { + const alertData = alertsDataMap[alertId]; + + if (!alertData) { + logger.warn(`Could not find alert data for ${alertId}`); + } + + const event: Mutable = { + ...alertData, + ...ruleExecutorData, + [TIMESTAMP]: timestamp, + [EVENT_KIND]: 'event', + [OWNER]: rule.consumer, + [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[ALERT_START] = started; + event[ALERT_UUID] = alertUuid; + + 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; + }); + + if (eventsToIndex.length) { + const alertEvents: Map = new Map(); + + for (const event of eventsToIndex) { + const uuid = event[ALERT_UUID]!; + let storedEvent = alertEvents.get(uuid); + if (!storedEvent) { + storedEvent = event; + } + alertEvents.set(uuid, { + ...storedEvent, + [EVENT_KIND]: 'signal', + }); + } + logger.debug(`Preparing to index ${eventsToIndex.length} alerts.`); + + if (ruleDataClient.isWriteEnabled()) { + await ruleDataClient.getWriter().bulk({ + body: eventsToIndex + .flatMap((event) => [{ index: {} }, event]) + .concat( + Array.from(alertEvents.values()).flatMap((event) => [ + { index: { _id: event[ALERT_UUID]! } }, + event, + ]) + ), + }); + } + } + + const nextTrackedAlerts = Object.fromEntries( + eventsToIndex + .filter((event) => event[ALERT_STATUS] !== 'closed') + .map((event) => { + const alertId = event[ALERT_ID]!; + const alertUuid = event[ALERT_UUID]!; + const started = new Date(event[ALERT_START]!).toISOString(); + return [alertId, { alertId, alertUuid, started }]; + }) + ); + + return { + wrapped: nextWrappedState ?? ({} as State), + trackedAlerts: ruleDataClient.isWriteEnabled() ? nextTrackedAlerts : {}, + }; +}; 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 a37ba9ef566366..3e7fbbe5cbc59f 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 @@ -38,11 +38,11 @@ function createRule() { }); nextAlerts = []; }, - id: 'test_type', + id: 'ruleTypeId', minimumLicenseRequired: 'basic', isExportable: true, - name: 'Test type', - producer: 'test', + name: 'ruleTypeName', + producer: 'producer', actionVariables: { context: [], params: [], @@ -195,11 +195,11 @@ describe('createLifecycleRuleTypeFactory', () => { "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-java", "kibana.rac.alert.owner": "consumer", - "kibana.rac.alert.producer": "test", + "kibana.rac.alert.producer": "producer", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", - "rule.category": "Test type", - "rule.id": "test_type", + "rule.category": "ruleTypeName", + "rule.id": "ruleTypeId", "rule.name": "name", "rule.uuid": "alertId", "service.name": "opbeans-java", @@ -214,11 +214,11 @@ describe('createLifecycleRuleTypeFactory', () => { "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-node", "kibana.rac.alert.owner": "consumer", - "kibana.rac.alert.producer": "test", + "kibana.rac.alert.producer": "producer", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", - "rule.category": "Test type", - "rule.id": "test_type", + "rule.category": "ruleTypeName", + "rule.id": "ruleTypeId", "rule.name": "name", "rule.uuid": "alertId", "service.name": "opbeans-node", @@ -233,11 +233,11 @@ describe('createLifecycleRuleTypeFactory', () => { "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-java", "kibana.rac.alert.owner": "consumer", - "kibana.rac.alert.producer": "test", + "kibana.rac.alert.producer": "producer", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", - "rule.category": "Test type", - "rule.id": "test_type", + "rule.category": "ruleTypeName", + "rule.id": "ruleTypeId", "rule.name": "name", "rule.uuid": "alertId", "service.name": "opbeans-java", @@ -252,11 +252,11 @@ describe('createLifecycleRuleTypeFactory', () => { "kibana.rac.alert.duration.us": 0, "kibana.rac.alert.id": "opbeans-node", "kibana.rac.alert.owner": "consumer", - "kibana.rac.alert.producer": "test", + "kibana.rac.alert.producer": "producer", "kibana.rac.alert.start": "2021-06-16T09:01:00.000Z", "kibana.rac.alert.status": "open", - "rule.category": "Test type", - "rule.id": "test_type", + "rule.category": "ruleTypeName", + "rule.id": "ruleTypeId", "rule.name": "name", "rule.uuid": "alertId", "service.name": "opbeans-node", 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 index 34045a2a905f8f..cf1be1bd32013b 100644 --- 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 @@ -5,51 +5,26 @@ * 2.0. */ import { Logger } from '@kbn/logging'; -import { isLeft } from 'fp-ts/lib/Either'; -import * as t from 'io-ts'; -import { Mutable } from 'utility-types'; -import v4 from 'uuid/v4'; -import { AlertInstance } from '../../../alerting/server'; import { RuleDataClient } from '..'; import { AlertInstanceContext, AlertInstanceState, AlertTypeParams, + AlertTypeState, } from '../../../alerting/common'; -import { - ALERT_DURATION, - ALERT_END, - ALERT_ID, - ALERT_START, - ALERT_STATUS, - ALERT_UUID, - EVENT_ACTION, - EVENT_KIND, - OWNER, - RULE_UUID, - TIMESTAMP, -} from '../../common/technical_rule_data_field_names'; +import { AlertInstance } from '../../../alerting/server'; import { AlertTypeWithExecutor } from '../types'; -import { ParsedTechnicalFields, parseTechnicalFields } from '../../common/parse_technical_fields'; -import { getRuleExecutorData } from './get_rule_executor_data'; +import { createLifecycleExecutor } from './create_lifecycle_executor'; export type LifecycleAlertService> = (alert: { id: string; fields: Record; }) => AlertInstance; -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), -}); - -type CreateLifecycleRuleTypeFactory = (options: { +export const createLifecycleRuleTypeFactory = ({ + logger, + ruleDataClient, +}: { ruleDataClient: RuleDataClient; logger: Logger; }) => < @@ -58,216 +33,17 @@ type CreateLifecycleRuleTypeFactory = (options: { TServices extends { alertWithLifecycle: LifecycleAlertService } >( type: AlertTypeWithExecutor -) => AlertTypeWithExecutor; - -export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ - logger, - ruleDataClient, -}) => (type) => { +): AlertTypeWithExecutor => { + const createBoundLifecycleExecutor = createLifecycleExecutor(logger, ruleDataClient); + const executor = createBoundLifecycleExecutor< + TParams, + AlertTypeState, + AlertInstanceState, + TAlertInstanceContext, + string + >(type.executor as any); return { ...type, - executor: async (options) => { - const { - services: { alertInstanceFactory }, - state: previousState, - rule, - } = options; - - const ruleExecutorData = getRuleExecutorData(type, options); - - const decodedState = wrappedStateRt.decode(previousState); - - const state = isLeft(decodedState) - ? { - wrapped: previousState, - trackedAlerts: {}, - } - : decodedState.right; - - const currentAlerts: Record = {}; - - const timestamp = options.startedAt.toISOString(); - - const nextWrappedState = await type.executor({ - ...options, - state: state.wrapped, - services: { - ...options.services, - alertWithLifecycle: ({ id, fields }) => { - currentAlerts[id] = { - ...fields, - [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< - string, - { - [ALERT_ID]: string; - } - > = { - ...currentAlerts, - }; - - if (trackedAlertStatesOfRecovered.length) { - const { hits } = await ruleDataClient.getReader().search({ - body: { - query: { - bool: { - filter: [ - { - term: { - [RULE_UUID]: ruleExecutorData[RULE_UUID], - }, - }, - { - terms: { - [ALERT_UUID]: trackedAlertStatesOfRecovered.map( - (trackedAlertState) => trackedAlertState.alertUuid - ), - }, - }, - ], - }, - }, - size: trackedAlertStatesOfRecovered.length, - collapse: { - field: ALERT_UUID, - }, - _source: false, - fields: [{ field: '*', include_unmapped: true }], - sort: { - [TIMESTAMP]: 'desc' as const, - }, - }, - allow_no_indices: true, - }); - - hits.hits.forEach((hit) => { - const fields = parseTechnicalFields(hit.fields); - const alertId = fields[ALERT_ID]!; - alertsDataMap[alertId] = { - ...fields, - [ALERT_ID]: alertId, - }; - }); - } - - const eventsToIndex = allAlertIds.map((alertId) => { - const alertData = alertsDataMap[alertId]; - - if (!alertData) { - logger.warn(`Could not find alert data for ${alertId}`); - } - - const event: Mutable = { - ...alertData, - ...ruleExecutorData, - [TIMESTAMP]: timestamp, - [EVENT_KIND]: 'event', - [OWNER]: rule.consumer, - [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[ALERT_START] = started; - event[ALERT_UUID] = alertUuid; - - 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; - }); - - if (eventsToIndex.length) { - const alertEvents: Map = new Map(); - - for (const event of eventsToIndex) { - const uuid = event[ALERT_UUID]!; - let storedEvent = alertEvents.get(uuid); - if (!storedEvent) { - storedEvent = event; - } - alertEvents.set(uuid, { - ...storedEvent, - [EVENT_KIND]: 'signal', - }); - } - logger.debug(`Preparing to index ${eventsToIndex.length} alerts.`); - - if (ruleDataClient.isWriteEnabled()) { - await ruleDataClient.getWriter().bulk({ - body: eventsToIndex - .flatMap((event) => [{ index: {} }, event]) - .concat( - Array.from(alertEvents.values()).flatMap((event) => [ - { index: { _id: event[ALERT_UUID]! } }, - event, - ]) - ), - }); - } - } - - const nextTrackedAlerts = Object.fromEntries( - eventsToIndex - .filter((event) => event[ALERT_STATUS] !== 'closed') - .map((event) => { - const alertId = event[ALERT_ID]!; - const alertUuid = event[ALERT_UUID]!; - const started = new Date(event[ALERT_START]!).toISOString(); - return [alertId, { alertId, alertUuid, started }]; - }) - ); - - return { - wrapped: nextWrappedState ?? {}, - trackedAlerts: ruleDataClient.isWriteEnabled() ? nextTrackedAlerts : {}, - }; - }, + executor: executor as any, }; }; 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 index 1ea640add7b48f..7cb02428322a65 100644 --- 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 @@ -5,6 +5,7 @@ * 2.0. */ +import { AlertExecutorOptions } from '../../../alerting/server'; import { PRODUCER, RULE_CATEGORY, @@ -37,3 +38,14 @@ export function getRuleExecutorData( [PRODUCER]: type.producer, }; } + +export function getRuleData(options: AlertExecutorOptions) { + return { + [RULE_ID]: options.rule.ruleTypeId, + [RULE_UUID]: options.alertId, + [RULE_CATEGORY]: options.rule.ruleTypeName, + [RULE_NAME]: options.rule.name, + [TAGS]: options.tags, + [PRODUCER]: options.rule.producer, + }; +} diff --git a/x-pack/plugins/rule_registry/server/utils/rbac.ts b/x-pack/plugins/rule_registry/server/utils/rbac.ts index 812dbb84088123..e07c4394be2f10 100644 --- a/x-pack/plugins/rule_registry/server/utils/rbac.ts +++ b/x-pack/plugins/rule_registry/server/utils/rbac.ts @@ -9,9 +9,14 @@ * registering a new instance of the rule data client * in a new plugin will require updating the below data structure * to include the index name where the alerts as data will be written to. + * + * This doesn't work in combination with the `xpack.ruleRegistry.index` + * setting, with which the user can change the index prefix. */ export const mapConsumerToIndexName = { apm: '.alerts-observability-apm', + logs: '.alerts-observability.logs', + infrastructure: '.alerts-observability.metrics', observability: '.alerts-observability', siem: ['.alerts-security.alerts', '.siem-signals'], };