From bdde884d098f42a93630cc6d221258bdc3372e60 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 13 May 2021 17:12:47 +0200 Subject: [PATCH] [RAC] Decouple registry from alerts-as-data client (#98935) --- package.json | 3 +- packages/kbn-optimizer/limits.yml | 1 - packages/kbn-rule-data-utils/jest.config.js | 13 + packages/kbn-rule-data-utils/package.json | 13 + packages/kbn-rule-data-utils/src/index.ts | 9 + .../src/technical_field_names.ts | 77 ++++ packages/kbn-rule-data-utils/tsconfig.json | 19 + typings/elasticsearch/search.d.ts | 6 +- x-pack/plugins/apm/kibana.json | 12 +- .../public/application/application.test.tsx | 8 +- .../plugins/apm/public/application/csmApp.tsx | 19 +- .../plugins/apm/public/application/index.tsx | 13 +- .../alerting/register_apm_alerts.ts | 98 +++--- .../ErrorGroupDetails/Distribution/index.tsx | 3 +- .../app/Home/__snapshots__/Home.test.tsx.snap | 14 +- .../service_icons/alert_details.tsx | 30 +- .../charts/helper/get_alert_annotations.tsx | 16 +- .../shared/charts/latency_chart/index.tsx | 5 +- .../transaction_error_rate_chart/index.tsx | 3 +- .../context/apm_plugin/apm_plugin_context.tsx | 5 +- .../apm_plugin/mock_apm_plugin_context.tsx | 9 +- x-pack/plugins/apm/public/plugin.ts | 21 +- .../server/lib/alerts/register_apm_alerts.ts | 6 +- .../alerts/register_error_count_alert_type.ts | 24 +- ...egister_transaction_duration_alert_type.ts | 252 +++++++------- ...transaction_duration_anomaly_alert_type.ts | 31 +- ...ister_transaction_error_rate_alert_type.ts | 24 +- .../apm/server/lib/alerts/test_utils/index.ts | 24 +- .../server/lib/services/get_service_alerts.ts | 18 +- x-pack/plugins/apm/server/plugin.ts | 103 +++++- .../server/routes/register_routes/index.ts | 6 +- x-pack/plugins/apm/server/routes/services.ts | 19 +- x-pack/plugins/apm/server/routes/typings.ts | 4 +- x-pack/plugins/apm/server/types.ts | 9 + .../rules/observability_rule_field_map.ts | 22 -- .../common/utils/formatters/duration.ts | 3 + .../common/utils/formatters/formatters.ts | 2 + x-pack/plugins/observability/kibana.json | 3 +- .../public/application/application.test.tsx | 4 +- .../public/application/index.tsx | 9 +- .../components/app/section/apm/index.test.tsx | 8 +- .../components/app/section/ux/index.test.tsx | 4 +- .../public/context/plugin_context.tsx | 5 +- .../public/hooks/use_time_range.test.ts | 6 +- x-pack/plugins/observability/public/index.ts | 3 +- .../public/pages/alerts/alerts.stories.tsx | 4 +- .../alerts_flyout/alerts_flyout.stories.tsx | 41 +-- .../pages/alerts/alerts_flyout/index.tsx | 20 +- .../public/pages/alerts/alerts_table.tsx | 10 +- .../public/pages/alerts/example_data.ts | 60 ++-- .../public/pages/alerts/index.tsx | 30 +- .../pages/overview/overview.stories.tsx | 4 +- x-pack/plugins/observability/public/plugin.ts | 25 +- ...create_observability_rule_type_registry.ts | 31 ++ .../public/rules/formatter_rule_registry.ts | 30 -- .../rules/observability_rule_registry_mock.ts | 17 - .../observability_rule_type_registry_mock.ts | 16 + .../public/utils/test_helper.tsx | 6 +- .../server/lib/rules/get_top_alerts.ts | 28 +- x-pack/plugins/observability/server/plugin.ts | 21 +- .../server/routes/register_routes.ts | 8 +- .../observability/server/routes/rules.ts | 27 +- .../observability/server/routes/types.ts | 4 +- x-pack/plugins/observability/server/types.ts | 6 - x-pack/plugins/rule_registry/README.md | 141 ++++++-- .../common/assets.ts} | 7 +- .../ecs_component_template.ts | 24 ++ .../technical_component_template.ts | 19 + .../field_maps}/ecs_field_map.ts | 0 .../field_maps/technical_rule_field_map.ts | 56 +++ .../index_templates/base_index_template.ts | 15 + .../default_lifecycle_policy.ts} | 4 +- .../common/field_map/base_rule_field_map.ts | 33 -- .../rule_registry/common/field_map/index.ts | 2 - x-pack/plugins/rule_registry/common/index.ts | 3 +- .../mapping_from_field_map.ts | 6 +- .../common/parse_technical_fields.ts | 25 ++ .../common/technical_rule_data_field_names.ts | 8 + x-pack/plugins/rule_registry/common/types.ts | 21 ++ x-pack/plugins/rule_registry/kibana.json | 3 +- x-pack/plugins/rule_registry/public/index.ts | 17 - x-pack/plugins/rule_registry/public/plugin.ts | 56 --- .../public/rule_registry/index.ts | 47 --- .../public/rule_registry/types.ts | 63 ---- x-pack/plugins/rule_registry/server/index.ts | 16 +- x-pack/plugins/rule_registry/server/plugin.ts | 48 +-- .../server/rule_data_client/index.ts | 132 +++++++ .../server/rule_data_client/types.ts | 44 +++ .../server/rule_data_plugin_service/index.ts | 158 +++++++++ .../index.ts | 179 ---------- .../types.ts | 57 --- .../server/rule_registry/index.ts | 328 ------------------ .../create_lifecycle_rule_type_factory.ts | 235 ------------- .../server/rule_registry/types.ts | 42 --- x-pack/plugins/rule_registry/server/types.ts | 106 ++---- .../create_lifecycle_rule_type_factory.ts | 246 +++++++++++++ .../server/utils/get_rule_executor_data.ts | 39 +++ .../utils/with_rule_data_client_factory.ts | 39 +++ .../test/apm_api_integration/configs/index.ts | 2 +- .../tests/alerts/rule_registry.ts | 229 ++++++++---- .../index_lifecycle_management/home_page.ts | 3 + .../index_lifecycle_management_page.ts | 5 + yarn.lock | 4 + 103 files changed, 1983 insertions(+), 1853 deletions(-) create mode 100644 packages/kbn-rule-data-utils/jest.config.js create mode 100644 packages/kbn-rule-data-utils/package.json create mode 100644 packages/kbn-rule-data-utils/src/index.ts create mode 100644 packages/kbn-rule-data-utils/src/technical_field_names.ts create mode 100644 packages/kbn-rule-data-utils/tsconfig.json delete mode 100644 x-pack/plugins/observability/common/rules/observability_rule_field_map.ts create mode 100644 x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts delete mode 100644 x-pack/plugins/observability/public/rules/formatter_rule_registry.ts delete mode 100644 x-pack/plugins/observability/public/rules/observability_rule_registry_mock.ts create mode 100644 x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts rename x-pack/plugins/{apm/server/lib/alerts/create_apm_lifecycle_rule_type.ts => rule_registry/common/assets.ts} (53%) create mode 100644 x-pack/plugins/rule_registry/common/assets/component_templates/ecs_component_template.ts create mode 100644 x-pack/plugins/rule_registry/common/assets/component_templates/technical_component_template.ts rename x-pack/plugins/rule_registry/common/{field_map => assets/field_maps}/ecs_field_map.ts (100%) create mode 100644 x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts create mode 100644 x-pack/plugins/rule_registry/common/assets/index_templates/base_index_template.ts rename x-pack/plugins/rule_registry/{server/rule_registry/defaults/ilm_policy.ts => common/assets/lifecycle_policies/default_lifecycle_policy.ts} (86%) delete mode 100644 x-pack/plugins/rule_registry/common/field_map/base_rule_field_map.ts rename x-pack/plugins/rule_registry/{server/rule_registry/field_map => common}/mapping_from_field_map.ts (79%) create mode 100644 x-pack/plugins/rule_registry/common/parse_technical_fields.ts create mode 100644 x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts create mode 100644 x-pack/plugins/rule_registry/common/types.ts delete mode 100644 x-pack/plugins/rule_registry/public/index.ts delete mode 100644 x-pack/plugins/rule_registry/public/plugin.ts delete mode 100644 x-pack/plugins/rule_registry/public/rule_registry/index.ts delete mode 100644 x-pack/plugins/rule_registry/public/rule_registry/types.ts create mode 100644 x-pack/plugins/rule_registry/server/rule_data_client/index.ts create mode 100644 x-pack/plugins/rule_registry/server/rule_data_client/types.ts create mode 100644 x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts delete mode 100644 x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/index.ts delete mode 100644 x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/types.ts delete mode 100644 x-pack/plugins/rule_registry/server/rule_registry/index.ts delete mode 100644 x-pack/plugins/rule_registry/server/rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory.ts delete mode 100644 x-pack/plugins/rule_registry/server/rule_registry/types.ts create mode 100644 x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts create mode 100644 x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts create mode 100644 x-pack/plugins/rule_registry/server/utils/with_rule_data_client_factory.ts diff --git a/package.json b/package.json index a8cfbb501364..d46617f2a6f2 100644 --- a/package.json +++ b/package.json @@ -136,9 +136,9 @@ "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", "@kbn/monaco": "link:packages/kbn-monaco", "@kbn/securitysolution-constants": "link:bazel-bin/packages/kbn-securitysolution-constants/npm_module", - "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", + "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@kbn/std": "link:bazel-bin/packages/kbn-std/npm_module", @@ -265,6 +265,7 @@ "json-stringify-safe": "5.0.1", "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", + "@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils", "kea": "^2.3.0", "leaflet": "1.5.1", "leaflet-draw": "0.4.14", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 08e90ed829d4..448b5ad650da 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -61,7 +61,6 @@ pageLoadAssetSize: remoteClusters: 51327 reporting: 183418 rollup: 97204 - ruleRegistry: 100000 savedObjects: 108518 savedObjectsManagement: 101836 savedObjectsTagging: 59482 diff --git a/packages/kbn-rule-data-utils/jest.config.js b/packages/kbn-rule-data-utils/jest.config.js new file mode 100644 index 000000000000..26cb39fe8b55 --- /dev/null +++ b/packages/kbn-rule-data-utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-rule-data-utils'], +}; diff --git a/packages/kbn-rule-data-utils/package.json b/packages/kbn-rule-data-utils/package.json new file mode 100644 index 000000000000..6f0b8439ec89 --- /dev/null +++ b/packages/kbn-rule-data-utils/package.json @@ -0,0 +1,13 @@ +{ + "name": "@kbn/rule-data-utils", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0", + "private": true, + "scripts": { + "build": "../../node_modules/.bin/tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + } +} diff --git a/packages/kbn-rule-data-utils/src/index.ts b/packages/kbn-rule-data-utils/src/index.ts new file mode 100644 index 000000000000..93a2538c7aa2 --- /dev/null +++ b/packages/kbn-rule-data-utils/src/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 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 * from './technical_field_names'; diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts new file mode 100644 index 000000000000..31779c9f08e8 --- /dev/null +++ b/packages/kbn-rule-data-utils/src/technical_field_names.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 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. + */ + +import { ValuesType } from 'utility-types'; + +const ALERT_NAMESPACE = 'kibana.rac.alert'; + +const TIMESTAMP = '@timestamp' as const; +const EVENT_KIND = 'event.kind' as const; +const EVENT_ACTION = 'event.action' as const; +const RULE_UUID = 'rule.uuid' as const; +const RULE_ID = 'rule.id' as const; +const RULE_NAME = 'rule.name' as const; +const RULE_CATEGORY = 'rule.category' as const; +const TAGS = 'tags' as const; +const PRODUCER = `${ALERT_NAMESPACE}.producer` as const; +const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; +const ALERT_UUID = `${ALERT_NAMESPACE}.uuid` as const; +const ALERT_START = `${ALERT_NAMESPACE}.start` as const; +const ALERT_END = `${ALERT_NAMESPACE}.end` as const; +const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; +const ALERT_SEVERITY_LEVEL = `${ALERT_NAMESPACE}.severity.level` as const; +const ALERT_SEVERITY_VALUE = `${ALERT_NAMESPACE}.severity.value` as const; +const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const; +const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; +const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const; + +const fields = { + TIMESTAMP, + EVENT_KIND, + EVENT_ACTION, + RULE_UUID, + RULE_ID, + RULE_NAME, + RULE_CATEGORY, + TAGS, + PRODUCER, + ALERT_ID, + ALERT_UUID, + ALERT_START, + ALERT_END, + ALERT_DURATION, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_STATUS, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +}; + +export { + TIMESTAMP, + EVENT_KIND, + EVENT_ACTION, + RULE_UUID, + RULE_ID, + RULE_NAME, + RULE_CATEGORY, + TAGS, + PRODUCER, + ALERT_ID, + ALERT_UUID, + ALERT_START, + ALERT_END, + ALERT_DURATION, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_STATUS, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +}; + +export type TechnicalRuleDataFieldName = ValuesType; diff --git a/packages/kbn-rule-data-utils/tsconfig.json b/packages/kbn-rule-data-utils/tsconfig.json new file mode 100644 index 000000000000..4b1262d11f3a --- /dev/null +++ b/packages/kbn-rule-data-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "outDir": "./target", + "stripInternal": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-rule-data-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/typings/elasticsearch/search.d.ts b/typings/elasticsearch/search.d.ts index c9bf3b1d8b7b..d75f31d38817 100644 --- a/typings/elasticsearch/search.d.ts +++ b/typings/elasticsearch/search.d.ts @@ -49,7 +49,7 @@ type ValueTypeOfField = T extends Record type MaybeArray = T | T[]; -type Fields = MaybeArray; +type Fields = Exclude['body']['fields'], undefined>; type DocValueFields = MaybeArray; export type SearchHit< @@ -58,7 +58,7 @@ export type SearchHit< TDocValueFields extends DocValueFields | undefined = undefined > = Omit & (TSource extends false ? {} : { _source: TSource }) & - (TFields extends estypes.Fields + (TFields extends Fields ? { fields: Partial, unknown[]>>; } @@ -77,7 +77,7 @@ type HitsOf< > = Array< SearchHit< TOptions extends { _source: false } ? undefined : TDocument, - TOptions extends { fields: estypes.Fields } ? TOptions['fields'] : undefined, + TOptions extends { fields: Fields } ? TOptions['fields'] : undefined, TOptions extends { docvalue_fields: DocValueFields } ? TOptions['docvalue_fields'] : undefined > >; diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 28e4a7b36e74..76d544c3bc6f 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -10,7 +10,8 @@ "triggersActionsUi", "embeddable", "infra", - "observability" + "observability", + "ruleRegistry" ], "optionalPlugins": [ "spaces", @@ -26,8 +27,13 @@ ], "server": true, "ui": true, - "configPath": ["xpack", "apm"], - "extraPublicDirs": ["public/style/variables"], + "configPath": [ + "xpack", + "apm" + ], + "extraPublicDirs": [ + "public/style/variables" + ], "requiredBundles": [ "home", "kibanaReact", diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index e6415f76c60d..4ec654a6c0bf 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -39,7 +39,11 @@ describe('renderApp', () => { }); it('renders the app', () => { - const { core, config, apmRuleRegistry } = mockApmPluginContextValue; + const { + core, + config, + observabilityRuleTypeRegistry, + } = mockApmPluginContextValue; const plugins = { licensing: { license$: new Observable() }, triggersActionsUi: { actionTypeRegistry: {}, alertTypeRegistry: {} }, @@ -92,7 +96,7 @@ describe('renderApp', () => { appMountParameters: params as any, pluginsStart: startDeps as any, config, - apmRuleRegistry, + observabilityRuleTypeRegistry, }); }); diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 17905074cfec..11a2777f47f6 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -12,6 +12,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router } from 'react-router-dom'; import { DefaultTheme, ThemeProvider } from 'styled-components'; +import type { ObservabilityRuleTypeRegistry } from '../../../observability/public'; import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider, @@ -26,11 +27,7 @@ import { ApmPluginContext } from '../context/apm_plugin/apm_plugin_context'; import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; import { ConfigSchema } from '../index'; -import { - ApmPluginSetupDeps, - ApmPluginStartDeps, - ApmRuleRegistry, -} from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; @@ -77,14 +74,14 @@ export function CsmAppRoot({ deps, config, corePlugins: { embeddable, maps }, - apmRuleRegistry, + observabilityRuleTypeRegistry, }: { appMountParameters: AppMountParameters; core: CoreStart; deps: ApmPluginSetupDeps; config: ConfigSchema; corePlugins: ApmPluginStartDeps; - apmRuleRegistry: ApmRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; }) { const { history } = appMountParameters; const i18nCore = core.i18n; @@ -94,7 +91,7 @@ export function CsmAppRoot({ config, core, plugins, - apmRuleRegistry, + observabilityRuleTypeRegistry, }; return ( @@ -125,14 +122,14 @@ export const renderApp = ({ appMountParameters, config, corePlugins, - apmRuleRegistry, + observabilityRuleTypeRegistry, }: { core: CoreStart; deps: ApmPluginSetupDeps; appMountParameters: AppMountParameters; config: ConfigSchema; corePlugins: ApmPluginStartDeps; - apmRuleRegistry: ApmRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; }) => { const { element } = appMountParameters; @@ -151,7 +148,7 @@ export const renderApp = ({ deps={deps} config={config} corePlugins={corePlugins} - apmRuleRegistry={apmRuleRegistry} + observabilityRuleTypeRegistry={observabilityRuleTypeRegistry} />, element ); diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index acb55a02599f..e2a0bdb6b48b 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -13,6 +13,7 @@ import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import 'react-vis/dist/style.css'; import { DefaultTheme, ThemeProvider } from 'styled-components'; +import type { ObservabilityRuleTypeRegistry } from '../../../observability/public'; import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { ConfigSchema } from '../'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; @@ -30,11 +31,7 @@ import { import { LicenseProvider } from '../context/license/license_context'; import { UrlParamsProvider } from '../context/url_params_context/url_params_context'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; -import { - ApmPluginSetupDeps, - ApmPluginStartDeps, - ApmRuleRegistry, -} from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; import { createCallApmApi } from '../services/rest/createCallApmApi'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; import { setHelpExtension } from '../setHelpExtension'; @@ -112,14 +109,14 @@ export const renderApp = ({ appMountParameters, config, pluginsStart, - apmRuleRegistry, + observabilityRuleTypeRegistry, }: { coreStart: CoreStart; pluginsSetup: ApmPluginSetupDeps; appMountParameters: AppMountParameters; config: ConfigSchema; pluginsStart: ApmPluginStartDeps; - apmRuleRegistry: ApmRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; }) => { const { element } = appMountParameters; const apmPluginContextValue = { @@ -127,7 +124,7 @@ export const renderApp = ({ config, core: coreStart, plugins: pluginsSetup, - apmRuleRegistry, + observabilityRuleTypeRegistry, }; // render APM feedback link in global help menu diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 98c8b99411bc..7e788016baad 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -8,9 +8,19 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; import { stringify } from 'querystring'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_SEVERITY_LEVEL, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { AlertType } from '../../../common/alert_types'; -import type { ApmRuleRegistry } from '../../plugin'; + +// copied from elasticsearch_fieldnames.ts to limit page load bundle size +const SERVICE_ENVIRONMENT = 'service.environment'; +const SERVICE_NAME = 'service.name'; +const TRANSACTION_TYPE = 'transaction.type'; const format = ({ pathname, @@ -22,28 +32,32 @@ const format = ({ return `${pathname}?${stringify(query)}`; }; -export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { - apmRuleRegistry.registerType({ +export function registerApmAlerts( + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry +) { + observabilityRuleTypeRegistry.register({ id: AlertType.ErrorCount, description: i18n.translate('xpack.apm.alertTypes.errorCount.description', { defaultMessage: 'Alert when the number of errors in a service exceeds a defined threshold.', }), - format: ({ alert }) => { + format: ({ fields }) => { return { reason: i18n.translate('xpack.apm.alertTypes.errorCount.reason', { defaultMessage: `Error count is greater than {threshold} (current value is {measured}) for {serviceName}`, values: { - threshold: alert['kibana.observability.evaluation.threshold'], - measured: alert['kibana.observability.evaluation.value'], - serviceName: alert['service.name']!, + threshold: fields[ALERT_EVALUATION_THRESHOLD], + measured: fields[ALERT_EVALUATION_VALUE], + serviceName: String(fields[SERVICE_NAME][0]), }, }), link: format({ - pathname: `/app/apm/services/${alert['service.name']!}/errors`, + pathname: `/app/apm/services/${String( + fields[SERVICE_NAME][0] + )}/errors`, query: { - ...(alert['service.environment'] - ? { environment: alert['service.environment'] } + ...(fields[SERVICE_ENVIRONMENT]?.[0] + ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } : { environment: ENVIRONMENT_ALL.value }), }, }), @@ -71,7 +85,7 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { ), }); - apmRuleRegistry.registerType({ + observabilityRuleTypeRegistry.register({ id: AlertType.TransactionDuration, description: i18n.translate( 'xpack.apm.alertTypes.transactionDuration.description', @@ -80,28 +94,24 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { 'Alert when the latency of a specific transaction type in a service exceeds a defined threshold.', } ), - format: ({ alert, formatters: { asDuration } }) => ({ + format: ({ fields, formatters: { asDuration } }) => ({ reason: i18n.translate( 'xpack.apm.alertTypes.transactionDuration.reason', { defaultMessage: `Latency is above {threshold} (current value is {measured}) for {serviceName}`, values: { - threshold: asDuration( - alert['kibana.observability.evaluation.threshold'] - ), - measured: asDuration( - alert['kibana.observability.evaluation.value'] - ), - serviceName: alert['service.name']!, + threshold: asDuration(fields[ALERT_EVALUATION_THRESHOLD]), + measured: asDuration(fields[ALERT_EVALUATION_VALUE]), + serviceName: String(fields[SERVICE_NAME][0]), }, } ), link: format({ - pathname: `/app/apm/services/${alert['service.name']!}`, + pathname: `/app/apm/services/${fields[SERVICE_NAME][0]!}`, query: { - transactionType: alert['transaction.type']!, - ...(alert['service.environment'] - ? { environment: alert['service.environment'] } + transactionType: fields[TRANSACTION_TYPE][0]!, + ...(fields[SERVICE_ENVIRONMENT]?.[0] + ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } : { environment: ENVIRONMENT_ALL.value }), }, }), @@ -131,7 +141,7 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { ), }); - apmRuleRegistry.registerType({ + observabilityRuleTypeRegistry.register({ id: AlertType.TransactionErrorRate, description: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.description', @@ -140,30 +150,24 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { 'Alert when the rate of transaction errors in a service exceeds a defined threshold.', } ), - format: ({ alert, formatters: { asPercent } }) => ({ + format: ({ fields, formatters: { asPercent } }) => ({ reason: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.reason', { defaultMessage: `Transaction error rate is greater than {threshold} (current value is {measured}) for {serviceName}`, values: { - threshold: asPercent( - alert['kibana.observability.evaluation.threshold'], - 100 - ), - measured: asPercent( - alert['kibana.observability.evaluation.value'], - 100 - ), - serviceName: alert['service.name']!, + threshold: asPercent(fields[ALERT_EVALUATION_THRESHOLD], 100), + measured: asPercent(fields[ALERT_EVALUATION_VALUE], 100), + serviceName: String(fields[SERVICE_NAME][0]), }, } ), link: format({ - pathname: `/app/apm/services/${alert['service.name']!}`, + pathname: `/app/apm/services/${String(fields[SERVICE_NAME][0]!)}`, query: { - transactionType: alert['transaction.type']!, - ...(alert['service.environment'] - ? { environment: alert['service.environment'] } + transactionType: String(fields[TRANSACTION_TYPE][0]!), + ...(fields[SERVICE_ENVIRONMENT]?.[0] + ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } : { environment: ENVIRONMENT_ALL.value }), }, }), @@ -193,7 +197,7 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { ), }); - apmRuleRegistry.registerType({ + observabilityRuleTypeRegistry.register({ id: AlertType.TransactionDurationAnomaly, description: i18n.translate( 'xpack.apm.alertTypes.transactionDurationAnomaly.description', @@ -201,24 +205,24 @@ export function registerApmAlerts(apmRuleRegistry: ApmRuleRegistry) { defaultMessage: 'Alert when the latency of a service is abnormal.', } ), - format: ({ alert }) => ({ + format: ({ fields }) => ({ reason: i18n.translate( 'xpack.apm.alertTypes.transactionDurationAnomaly.reason', { defaultMessage: `{severityLevel} anomaly detected for {serviceName} (score was {measured})`, values: { - serviceName: alert['service.name'], - severityLevel: alert['kibana.rac.alert.severity.level'], - measured: alert['kibana.observability.evaluation.value'], + serviceName: String(fields[SERVICE_NAME][0]), + severityLevel: String(fields[ALERT_SEVERITY_LEVEL]), + measured: Number(fields[ALERT_EVALUATION_VALUE]), }, } ), link: format({ - pathname: `/app/apm/services/${alert['service.name']!}`, + pathname: `/app/apm/services/${String(fields[SERVICE_NAME][0])}`, query: { - transactionType: alert['transaction.type']!, - ...(alert['service.environment'] - ? { environment: alert['service.environment'] } + transactionType: String(fields[TRANSACTION_TYPE][0]), + ...(fields[SERVICE_ENVIRONMENT]?.[0] + ? { environment: String(fields[SERVICE_ENVIRONMENT][0]) } : { environment: ENVIRONMENT_ALL.value }), }, }), diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 19a567a3866b..16ac1a35666d 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -19,6 +19,7 @@ import { import { EuiTitle } from '@elastic/eui'; import d3 from 'd3'; import React from 'react'; +import { RULE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; @@ -115,7 +116,7 @@ export function ErrorDistribution({ distribution, title }: Props) { /> {getAlertAnnotations({ alerts: alerts?.filter( - (alert) => alert['rule.id'] === AlertType.ErrorCount + (alert) => alert[RULE_ID]?.[0] === AlertType.ErrorCount ), theme, })} diff --git a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index b1bcf561bed8..f13cce3fd9b4 100644 --- a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -4,10 +4,6 @@ exports[`Home component should render services 1`] = ` alert['kibana.rac.alert.id']! + const collapsedAlerts = uniqBy(alerts, (alert) => alert[ALERT_ID]![0]!).map( + (alert) => { + return parseTechnicalFields(alert); + } ); return ( {collapsedAlerts.map((alert) => { - const ruleType = apmRuleRegistry.getTypeByRuleId(alert['rule.id']!); + const formatter = observabilityRuleTypeRegistry.getFormatter( + alert[RULE_ID]! + ); const formatted = { link: undefined, - reason: alert['rule.name'], - ...(ruleType?.format?.({ - alert, + reason: alert[RULE_NAME], + ...(formatter?.({ + fields: alert, formatters: { asDuration, asPercent }, }) ?? {}), }; @@ -55,7 +65,7 @@ export function AlertDetails({ alerts }: AlertDetailProps) { : undefined; return ( - + {parsedLink ? ( @@ -79,7 +89,7 @@ export function AlertDetails({ alerts }: AlertDetailProps) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index 2c086dbb1722..e906707730ba 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -9,6 +9,13 @@ import { ValuesType } from 'utility-types'; import { RectAnnotation } from '@elastic/charts'; import { EuiTheme } from 'src/plugins/kibana_react/common'; import { rgba } from 'polished'; +import { + ALERT_DURATION, + RULE_ID, + ALERT_START, + ALERT_UUID, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { parseTechnicalFields } from '../../../../../../rule_registry/common'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; type Alert = ValuesType< @@ -30,10 +37,11 @@ export function getAlertAnnotations({ theme: EuiTheme; }) { return alerts?.flatMap((alert) => { - const uuid = alert['kibana.rac.alert.uuid']!; - const start = new Date(alert['kibana.rac.alert.start']!).getTime(); - const end = start + alert['kibana.rac.alert.duration.us']! / 1000; - const color = getAlertColor({ ruleId: alert['rule.id']!, theme }); + const parsed = parseTechnicalFields(alert); + const uuid = parsed[ALERT_UUID]!; + const start = new Date(parsed[ALERT_START]!).getTime(); + const end = start + parsed[ALERT_DURATION]! / 1000; + const color = getAlertColor({ ruleId: parsed[RULE_ID]!, theme }); return [ - alert['rule.id'] === AlertType.TransactionDuration || - alert['rule.id'] === AlertType.TransactionDurationAnomaly + alert[RULE_ID]?.[0] === AlertType.TransactionDuration || + alert[RULE_ID]?.[0] === AlertType.TransactionDurationAnomaly )} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 9aefa55aaaa3..7eceaf5ca8e5 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -9,6 +9,7 @@ import { EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useParams } from 'react-router-dom'; +import { RULE_ID } from '../../../../../../rule_registry/common/technical_rule_data_field_names'; import { AlertType } from '../../../../../common/alert_types'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asPercent } from '../../../../../common/utils/formatters'; @@ -152,7 +153,7 @@ export function TransactionErrorRateChart({ yDomain={{ min: 0, max: 1 }} customTheme={comparisonChartThem} alerts={alerts.filter( - (alert) => alert['rule.id'] === AlertType.TransactionErrorRate + (alert) => alert[RULE_ID]?.[0] === AlertType.TransactionErrorRate )} /> diff --git a/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx index 175471e7ae81..ec42a1178327 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/apm_plugin_context.tsx @@ -7,8 +7,9 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import { createContext } from 'react'; +import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; import { ConfigSchema } from '../..'; -import { ApmPluginSetupDeps, ApmRuleRegistry } from '../../plugin'; +import { ApmPluginSetupDeps } from '../../plugin'; import { MapsStartApi } from '../../../../maps/public'; export interface ApmPluginContextValue { @@ -16,7 +17,7 @@ export interface ApmPluginContextValue { config: ConfigSchema; core: CoreStart; plugins: ApmPluginSetupDeps & { maps?: MapsStartApi }; - apmRuleRegistry: ApmRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; } export const ApmPluginContext = createContext({} as ApmPluginContextValue); diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 07da5ea7f6c1..a16f81826636 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -7,12 +7,12 @@ import React, { ReactNode } from 'react'; import { Observable, of } from 'rxjs'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public'; import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context'; import { ConfigSchema } from '../..'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { createCallApmApi } from '../../services/rest/createCallApmApi'; import { MlUrlGenerator } from '../../../../ml/public'; -import { ApmRuleRegistry } from '../../plugin'; const uiSettings: Record = { [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ @@ -77,11 +77,6 @@ const mockCore = { }, }; -const mockApmRuleRegistry = ({ - getTypeByRuleId: () => undefined, - registerType: () => undefined, -} as unknown) as ApmRuleRegistry; - const mockConfig: ConfigSchema = { serviceMapEnabled: true, ui: { @@ -116,7 +111,7 @@ export const mockApmPluginContextValue = { config: mockConfig, core: mockCore, plugins: mockPlugin, - apmRuleRegistry: mockApmRuleRegistry, + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), }; export function MockApmPluginContextWrapper({ diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index f7bbe647d8e3..b493363d98f7 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -34,19 +34,15 @@ import type { HasDataParams, ObservabilityPublicSetup, } from '../../observability/public'; -import { FormatterRuleRegistry } from '../../observability/public'; import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; -import { apmRuleRegistrySettings } from '../common/rules/apm_rule_registry_settings'; -import type { APMRuleFieldMap } from '../common/rules/apm_rule_field_map'; import { registerApmAlerts } from './components/alerting/register_apm_alerts'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; export type ApmPluginSetup = ReturnType; -export type ApmRuleRegistry = ApmPluginSetup['ruleRegistry']; export type ApmPluginStart = void; @@ -87,11 +83,6 @@ export class ApmPlugin implements Plugin { pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); } - const apmRuleRegistry = plugins.observability.ruleRegistry.create({ - ...apmRuleRegistrySettings, - fieldMap: {} as APMRuleFieldMap, - ctor: FormatterRuleRegistry, - }); const getApmDataHelper = async () => { const { fetchObservabilityOverviewPageData, @@ -127,6 +118,8 @@ export class ApmPlugin implements Plugin { return { fetchUxOverviewDate, hasRumData }; }; + const { observabilityRuleTypeRegistry } = plugins.observability; + plugins.observability.dashboard.register({ appName: 'ux', hasData: async (params?: HasDataParams) => { @@ -187,12 +180,12 @@ export class ApmPlugin implements Plugin { appMountParameters, config, pluginsStart: pluginsStart as ApmPluginStartDeps, - apmRuleRegistry, + observabilityRuleTypeRegistry, }); }, }); - registerApmAlerts(apmRuleRegistry); + registerApmAlerts(observabilityRuleTypeRegistry); core.application.register({ id: 'ux', @@ -231,14 +224,12 @@ export class ApmPlugin implements Plugin { appMountParameters, config, corePlugins: corePlugins as ApmPluginStartDeps, - apmRuleRegistry, + observabilityRuleTypeRegistry, }); }, }); - return { - ruleRegistry: apmRuleRegistry, - }; + return {}; } public start(core: CoreStart, plugins: ApmPluginStartDeps) { toggleAppLinkInNav(core, this.initializerContext.config.get()); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts index 9a362efa90ac..022fad6fa784 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts @@ -7,17 +7,19 @@ import { Observable } from 'rxjs'; import { Logger } from 'kibana/server'; +import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../alerting/server'; +import { RuleDataClient } from '../../../../rule_registry/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; import { registerErrorCountAlertType } from './register_error_count_alert_type'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type'; -import { APMRuleRegistry } from '../../plugin'; export interface RegisterRuleDependencies { - registry: APMRuleRegistry; + ruleDataClient: RuleDataClient; ml?: MlPluginSetup; + alerting: AlertingPluginSetupContract; config$: Observable; logger: Logger; } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 15ec5d0ef0bd..885b22ae343d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -7,6 +7,11 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; @@ -21,7 +26,6 @@ import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; -import { createAPMLifecycleRuleType } from './create_apm_lifecycle_rule_type'; const paramsSchema = schema.object({ windowSize: schema.number(), @@ -34,11 +38,18 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorCount]; export function registerErrorCountAlertType({ - registry, + alerting, + logger, + ruleDataClient, config$, }: RegisterRuleDependencies) { - registry.registerType( - createAPMLifecycleRuleType({ + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + ruleDataClient, + logger, + }); + + alerting.registerType( + createLifecycleRuleType({ id: AlertType.ErrorCount, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, @@ -146,9 +157,8 @@ export function registerErrorCountAlertType({ ? { [SERVICE_ENVIRONMENT]: environment } : {}), [PROCESSOR_EVENT]: ProcessorEvent.error, - 'kibana.observability.evaluation.value': errorCount, - 'kibana.observability.evaluation.threshold': - alertParams.threshold, + [ALERT_EVALUATION_VALUE]: errorCount, + [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index 4918a6cc892b..f77cc3ee930b 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -8,6 +8,11 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; import { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { parseEnvironmentUrlParam } from '../../../common/environment_filter_values'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { @@ -24,7 +29,6 @@ import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; -import { createAPMLifecycleRuleType } from './create_apm_lifecycle_rule_type'; const paramsSchema = schema.object({ serviceName: schema.string(), @@ -43,130 +47,142 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionDuration]; export function registerTransactionDurationAlertType({ - registry, + alerting, + ruleDataClient, config$, + logger, }: RegisterRuleDependencies) { - registry.registerType( - createAPMLifecycleRuleType({ - id: AlertType.TransactionDuration, - name: alertTypeConfig.name, - actionGroups: alertTypeConfig.actionGroups, - defaultActionGroupId: alertTypeConfig.defaultActionGroupId, - validate: { - params: paramsSchema, - }, - actionVariables: { - context: [ - apmActionVariables.serviceName, - apmActionVariables.transactionType, - apmActionVariables.environment, - apmActionVariables.threshold, - apmActionVariables.triggerValue, - apmActionVariables.interval, - ], - }, - producer: 'apm', - minimumLicenseRequired: 'basic', - executor: async ({ services, params }) => { - const config = await config$.pipe(take(1)).toPromise(); - const alertParams = params; - const indices = await getApmIndices({ - config, - savedObjectsClient: services.savedObjectsClient, - }); - - const searchParams = { - index: indices['apm_oss.transactionIndices'], - body: { - size: 0, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`, - }, + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + ruleDataClient, + logger, + }); + + const type = createLifecycleRuleType({ + id: AlertType.TransactionDuration, + name: alertTypeConfig.name, + actionGroups: alertTypeConfig.actionGroups, + defaultActionGroupId: alertTypeConfig.defaultActionGroupId, + validate: { + params: paramsSchema, + }, + actionVariables: { + context: [ + apmActionVariables.serviceName, + apmActionVariables.transactionType, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, + apmActionVariables.interval, + ], + }, + producer: 'apm', + minimumLicenseRequired: 'basic', + executor: async ({ services, params }) => { + const config = await config$.pipe(take(1)).toPromise(); + const alertParams = params; + const indices = await getApmIndices({ + config, + savedObjectsClient: services.savedObjectsClient, + }); + + const searchParams = { + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`, }, }, - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { term: { [SERVICE_NAME]: alertParams.serviceName } }, - { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, - ...environmentQuery(alertParams.environment), - ] as QueryContainer[], - }, + }, + { + term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction }, + }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + { + term: { + [TRANSACTION_TYPE]: alertParams.transactionType, + }, + }, + ...environmentQuery(alertParams.environment), + ] as QueryContainer[], }, - aggs: { - latency: - alertParams.aggregationType === 'avg' - ? { avg: { field: TRANSACTION_DURATION } } - : { - percentiles: { - field: TRANSACTION_DURATION, - percents: [ - alertParams.aggregationType === '95th' ? 95 : 99, - ], - }, + }, + aggs: { + latency: + alertParams.aggregationType === 'avg' + ? { avg: { field: TRANSACTION_DURATION } } + : { + percentiles: { + field: TRANSACTION_DURATION, + percents: [ + alertParams.aggregationType === '95th' ? 95 : 99, + ], }, - }, + }, }, - }; - - const response = await alertingEsClient({ - scopedClusterClient: services.scopedClusterClient, - params: searchParams, - }); - - if (!response.aggregations) { - return {}; - } - - const { latency } = response.aggregations; - - const transactionDuration = - 'values' in latency - ? Object.values(latency.values)[0] - : latency?.value; - - const threshold = alertParams.threshold * 1000; - - if (transactionDuration && transactionDuration > threshold) { - const durationFormatter = getDurationFormatter(transactionDuration); - const transactionDurationFormatted = durationFormatter( - transactionDuration - ).formatted; - - const environmentParsed = parseEnvironmentUrlParam( - alertParams.environment - ); - - services - .alertWithLifecycle({ - id: `${AlertType.TransactionDuration}_${environmentParsed.text}`, - fields: { - [SERVICE_NAME]: alertParams.serviceName, - ...(environmentParsed.esFieldValue - ? { [SERVICE_ENVIRONMENT]: environmentParsed.esFieldValue } - : {}), - [TRANSACTION_TYPE]: alertParams.transactionType, - [PROCESSOR_EVENT]: ProcessorEvent.transaction, - 'kibana.observability.evaluation.value': transactionDuration, - 'kibana.observability.evaluation.threshold': - alertParams.threshold * 1000, - }, - }) - .scheduleActions(alertTypeConfig.defaultActionGroupId, { - transactionType: alertParams.transactionType, - serviceName: alertParams.serviceName, - environment: environmentParsed.text, - threshold, - triggerValue: transactionDurationFormatted, - interval: `${alertParams.windowSize}${alertParams.windowUnit}`, - }); - } + }, + }; + const response = await alertingEsClient({ + scopedClusterClient: services.scopedClusterClient, + params: searchParams, + }); + + if (!response.aggregations) { return {}; - }, - }) - ); + } + + const { latency } = response.aggregations; + + const transactionDuration = + 'values' in latency ? Object.values(latency.values)[0] : latency?.value; + + const threshold = alertParams.threshold * 1000; + + if (transactionDuration && transactionDuration > threshold) { + const durationFormatter = getDurationFormatter(transactionDuration); + const transactionDurationFormatted = durationFormatter( + transactionDuration + ).formatted; + + const environmentParsed = parseEnvironmentUrlParam( + alertParams.environment + ); + + services + .alertWithLifecycle({ + id: `${AlertType.TransactionDuration}_${environmentParsed.text}`, + fields: { + [SERVICE_NAME]: alertParams.serviceName, + ...(environmentParsed.esFieldValue + ? { + [SERVICE_ENVIRONMENT]: environmentParsed.esFieldValue, + } + : {}), + [TRANSACTION_TYPE]: alertParams.transactionType, + [PROCESSOR_EVENT]: ProcessorEvent.transaction, + [ALERT_EVALUATION_VALUE]: transactionDuration, + [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold * 1000, + }, + }) + .scheduleActions(alertTypeConfig.defaultActionGroupId, { + transactionType: alertParams.transactionType, + serviceName: alertParams.serviceName, + environment: environmentParsed.text, + threshold, + triggerValue: transactionDurationFormatted, + interval: `${alertParams.windowSize}${alertParams.windowUnit}`, + }); + } + + return {}; + }, + }); + + alerting.registerType(type); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 67ff7cdb8e4e..399fb9a216ef 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -9,6 +9,13 @@ import { schema } from '@kbn/config-schema'; import { compact } from 'lodash'; import { ESSearchResponse } from 'typings/elasticsearch'; import { QueryContainer } from '@elastic/elasticsearch/api/types'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ProcessorEvent } from '../../../common/processor_event'; import { getSeverity } from '../../../common/anomaly_detection'; import { @@ -29,7 +36,6 @@ import { getMLJobs } from '../service_map/get_service_anomalies'; import { apmActionVariables } from './action_variables'; import { RegisterRuleDependencies } from './register_apm_alerts'; import { parseEnvironmentUrlParam } from '../../../common/environment_filter_values'; -import { createAPMLifecycleRuleType } from './create_apm_lifecycle_rule_type'; const paramsSchema = schema.object({ serviceName: schema.maybe(schema.string()), @@ -49,11 +55,18 @@ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionDurationAnomaly]; export function registerTransactionDurationAnomalyAlertType({ - registry, + logger, + ruleDataClient, + alerting, ml, }: RegisterRuleDependencies) { - registry.registerType( - createAPMLifecycleRuleType({ + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + logger, + ruleDataClient, + }); + + alerting.registerType( + createLifecycleRuleType({ id: AlertType.TransactionDurationAnomaly, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, @@ -190,7 +203,7 @@ export function registerTransactionDurationAnomalyAlertType({ const job = mlJobs.find((j) => j.job_id === latest.job_id); if (!job) { - services.logger.warn( + logger.warn( `Could not find matching job for job id ${latest.job_id}` ); return undefined; @@ -231,10 +244,10 @@ export function registerTransactionDurationAnomalyAlertType({ : {}), [TRANSACTION_TYPE]: transactionType, [PROCESSOR_EVENT]: ProcessorEvent.transaction, - 'kibana.rac.alert.severity.level': severityLevel, - 'kibana.rac.alert.severity.value': score, - 'kibana.observability.evaluation.value': score, - 'kibana.observability.evaluation.threshold': threshold, + [ALERT_SEVERITY_LEVEL]: severityLevel, + [ALERT_SEVERITY_VALUE]: score, + [ALERT_EVALUATION_VALUE]: score, + [ALERT_EVALUATION_THRESHOLD]: threshold, }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index bead17e308f0..4d6a0685fd37 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -7,6 +7,11 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; +import { + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { EVENT_OUTCOME, @@ -22,7 +27,6 @@ import { environmentQuery } from '../../../server/utils/queries'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; -import { createAPMLifecycleRuleType } from './create_apm_lifecycle_rule_type'; import { RegisterRuleDependencies } from './register_apm_alerts'; const paramsSchema = schema.object({ @@ -37,11 +41,18 @@ const paramsSchema = schema.object({ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionErrorRate]; export function registerTransactionErrorRateAlertType({ - registry, + alerting, + ruleDataClient, + logger, config$, }: RegisterRuleDependencies) { - registry.registerType( - createAPMLifecycleRuleType({ + const createLifecycleRuleType = createLifecycleRuleTypeFactory({ + ruleDataClient, + logger, + }); + + alerting.registerType( + createLifecycleRuleType({ id: AlertType.TransactionErrorRate, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, @@ -183,9 +194,8 @@ export function registerTransactionErrorRateAlertType({ ...(environment ? { [SERVICE_ENVIRONMENT]: environment } : {}), [TRANSACTION_TYPE]: transactionType, [PROCESSOR_EVENT]: ProcessorEvent.transaction, - 'kibana.observability.evaluation.value': errorRate, - 'kibana.observability.evaluation.threshold': - alertParams.threshold, + [ALERT_EVALUATION_VALUE]: errorRate, + [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { 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 37b3e282d0a5..ce1466bff01a 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 @@ -8,8 +8,9 @@ import { Logger } from 'kibana/server'; import { of } from 'rxjs'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import type { RuleDataClient } from '../../../../../rule_registry/server'; +import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../../alerting/server'; import { APMConfig } from '../../..'; -import { APMRuleRegistry } from '../../../plugin'; export const createRuleTypeMocks = () => { let alertExecutor: (...args: any[]) => Promise; @@ -27,19 +28,16 @@ export const createRuleTypeMocks = () => { error: jest.fn(), } as unknown) as Logger; - const registry = { + const alerting = { registerType: ({ executor }) => { alertExecutor = executor; }, - } as APMRuleRegistry; + } as AlertingPluginSetupContract; const scheduleActions = jest.fn(); const services = { scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), - scopedRuleRegistryClient: { - bulkIndex: jest.fn(), - }, alertInstanceFactory: jest.fn(() => ({ scheduleActions })), alertWithLifecycle: jest.fn(), logger: loggerMock, @@ -47,9 +45,21 @@ export const createRuleTypeMocks = () => { return { dependencies: { - registry, + alerting, config$: mockedConfig$, logger: loggerMock, + ruleDataClient: ({ + getReader: () => { + return { + search: jest.fn(), + }; + }, + getWriter: () => { + return { + bulk: jest.fn(), + }; + }, + } as unknown) as RuleDataClient, }, services, scheduleActions, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts index 6356731cc48d..f58452ce4d91 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts @@ -5,33 +5,30 @@ * 2.0. */ +import { ALERT_UUID } from '@kbn/rule-data-utils/target/technical_field_names'; +import { RuleDataClient } from '../../../../rule_registry/server'; import { SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import type { PromiseReturnType } from '../../../../observability/typings/common'; -import type { APMRuleRegistry } from '../../plugin'; import { environmentQuery, rangeQuery } from '../../utils/queries'; export async function getServiceAlerts({ - apmRuleRegistryClient, + ruleDataClient, start, end, serviceName, environment, transactionType, }: { - apmRuleRegistryClient: Exclude< - PromiseReturnType, - undefined - >; + ruleDataClient: RuleDataClient; start: number; end: number; serviceName: string; environment?: string; transactionType: string; }) { - const response = await apmRuleRegistryClient.search({ + const response = await ruleDataClient.getReader().search({ body: { query: { bool: { @@ -68,13 +65,14 @@ export async function getServiceAlerts({ size: 100, fields: ['*'], collapse: { - field: 'kibana.rac.alert.uuid', + field: ALERT_UUID, }, sort: { '@timestamp': 'desc', }, }, + allow_no_indices: true, }); - return response.events; + return response.hits.hits.map((hit) => hit.fields); } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index e12d08985583..44334889128c 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -16,7 +16,10 @@ import { Plugin, PluginInitializerContext, } from 'src/core/server'; -import { mapValues } from 'lodash'; +import { mapValues, once } from 'lodash'; +import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets'; +import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map'; +import { RuleDataClient } from '../../rule_registry/server'; import { APMConfig, APMXPackConfig } from '.'; import { mergeConfigs } from './index'; import { UI_SETTINGS } from '../../../../src/plugins/data/common'; @@ -42,10 +45,12 @@ import { } from './types'; import { registerRoutes } from './routes/register_routes'; import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository'; -import { apmRuleRegistrySettings } from '../common/rules/apm_rule_registry_settings'; -import { apmRuleFieldMap } from '../common/rules/apm_rule_field_map'; - -export type APMRuleRegistry = ReturnType['ruleRegistry']; +import { + PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../common/elasticsearch_fieldnames'; export class APMPlugin implements @@ -124,20 +129,81 @@ export class APMPlugin registerFeaturesUsage({ licensingPlugin: plugins.licensing }); - const apmRuleRegistry = plugins.observability.ruleRegistry.create({ - ...apmRuleRegistrySettings, - fieldMap: apmRuleFieldMap, + const getCoreStart = () => + core.getStartServices().then(([coreStart]) => coreStart); + + const ready = once(async () => { + const componentTemplateName = plugins.ruleRegistry.getFullAssetName( + 'apm-mappings' + ); + + if (!plugins.ruleRegistry.isWriteEnabled()) { + return; + } + + await plugins.ruleRegistry.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: mappingFromFieldMap({ + [SERVICE_NAME]: { + type: 'keyword', + }, + [SERVICE_ENVIRONMENT]: { + type: 'keyword', + }, + [TRANSACTION_TYPE]: { + type: 'keyword', + }, + [PROCESSOR_EVENT]: { + type: 'keyword', + }, + }), + }, + }, + }); + + await plugins.ruleRegistry.createOrUpdateIndexTemplate({ + name: plugins.ruleRegistry.getFullAssetName('apm-index-template'), + body: { + index_patterns: [ + plugins.ruleRegistry.getFullAssetName('observability-apm*'), + ], + composed_of: [ + plugins.ruleRegistry.getFullAssetName( + TECHNICAL_COMPONENT_TEMPLATE_NAME + ), + componentTemplateName, + ], + }, + }); + }); + + ready().catch((err) => { + this.logger!.error(err); + }); + + const ruleDataClient = new RuleDataClient({ + alias: plugins.ruleRegistry.getFullAssetName('observability-apm'), + getClusterClient: async () => { + const coreStart = await getCoreStart(); + return coreStart.elasticsearch.client.asInternalUser; + }, + ready, }); registerRoutes({ core: { setup: core, - start: () => core.getStartServices().then(([coreStart]) => coreStart), + start: getCoreStart, }, logger: this.logger, config: currentConfig, repository: getGlobalApmServerRouteRepository(), - apmRuleRegistry, + ruleDataClient, plugins: mapValues(plugins, (value, key) => { return { setup: value, @@ -157,12 +223,16 @@ export class APMPlugin savedObjectsClient: await getInternalSavedObjectsClient(core), config: await mergedConfig$.pipe(take(1)).toPromise(), }); - registerApmAlerts({ - registry: apmRuleRegistry, - ml: plugins.ml, - config$: mergedConfig$, - logger: this.logger!.get('rule'), - }); + + if (plugins.alerting) { + registerApmAlerts({ + ruleDataClient, + alerting: plugins.alerting, + ml: plugins.ml, + config$: mergedConfig$, + logger: this.logger!.get('rule'), + }); + } return { config$: mergedConfig$, @@ -193,7 +263,6 @@ export class APMPlugin }, }); }, - ruleRegistry: apmRuleRegistry, }; } diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts index f792e078c528..c9df12fd5820 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -39,14 +39,14 @@ export function registerRoutes({ plugins, logger, config, - apmRuleRegistry, + ruleDataClient, }: { core: APMRouteHandlerResources['core']; plugins: APMRouteHandlerResources['plugins']; logger: APMRouteHandlerResources['logger']; repository: ServerRouteRepository; config: APMRouteHandlerResources['config']; - apmRuleRegistry: APMRouteHandlerResources['apmRuleRegistry']; + ruleDataClient: APMRouteHandlerResources['ruleDataClient']; }) { const routes = repository.getRoutes(); @@ -99,7 +99,7 @@ export function registerRoutes({ }, validatedParams ), - apmRuleRegistry, + ruleDataClient, })) as any; if (Array.isArray(data)) { diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 54e59f2be7ae..4384d2be78ca 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -737,30 +737,15 @@ const serviceAlertsRoute = createApmServerRoute({ options: { tags: ['access:apm'], }, - handler: async ({ context, params, apmRuleRegistry }) => { - const alertsClient = context.alerting.getAlertsClient(); - + handler: async ({ context, params, ruleDataClient }) => { const { query: { start, end, environment, transactionType }, path: { serviceName }, } = params; - const apmRuleRegistryClient = await apmRuleRegistry.createScopedRuleRegistryClient( - { - alertsClient, - context, - } - ); - - if (!apmRuleRegistryClient) { - throw Boom.failedDependency( - 'xpack.ruleRegistry.unsafe.write.enabled is set to false' - ); - } - return { alerts: await getServiceAlerts({ - apmRuleRegistryClient, + ruleDataClient, start, end, serviceName, diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 602e1f3e0edb..13bd631085aa 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -12,11 +12,11 @@ import { KibanaRequest, CoreStart, } from 'src/core/server'; +import { RuleDataClient } from '../../../rule_registry/server'; import { AlertingApiRequestHandlerContext } from '../../../alerting/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; import { APMConfig } from '..'; import { APMPluginDependencies } from '../types'; -import { APMRuleRegistry } from '../plugin'; export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { licensing: LicensingApiRequestHandlerContext; @@ -62,5 +62,5 @@ export interface APMRouteHandlerResources { start: () => Promise[key]['start']>; }; }; - apmRuleRegistry: APMRuleRegistry; + ruleDataClient: RuleDataClient; } diff --git a/x-pack/plugins/apm/server/types.ts b/x-pack/plugins/apm/server/types.ts index dbc220f9f6b1..a5ba4f39b32b 100644 --- a/x-pack/plugins/apm/server/types.ts +++ b/x-pack/plugins/apm/server/types.ts @@ -7,6 +7,10 @@ import { ValuesType } from 'utility-types'; import { Observable } from 'rxjs'; import { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server'; +import { + RuleRegistryPluginSetupContract, + RuleRegistryPluginStartContract, +} from '../../rule_registry/server'; import { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, @@ -115,6 +119,10 @@ interface DependencyMap { setup: DataPluginSetup; start: DataPluginStart; }; + ruleRegistry: { + setup: RuleRegistryPluginSetupContract; + start: RuleRegistryPluginStartContract; + }; } const requiredDependencies = [ @@ -126,6 +134,7 @@ const requiredDependencies = [ 'embeddable', 'infra', 'observability', + 'ruleRegistry', ] as const; const optionalDependencies = [ diff --git a/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts b/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts deleted file mode 100644 index 370f5d4ef79f..000000000000 --- a/x-pack/plugins/observability/common/rules/observability_rule_field_map.ts +++ /dev/null @@ -1,22 +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 { ecsFieldMap, pickWithPatterns } from '../../../rule_registry/common'; - -export const observabilityRuleFieldMap = { - ...pickWithPatterns(ecsFieldMap, 'host.name', 'service.name'), - 'kibana.observability.evaluation.value': { - type: 'scaled_float' as const, - scaling_factor: 1000, - }, - 'kibana.observability.evaluation.threshold': { - type: 'scaled_float' as const, - scaling_factor: 1000, - }, -}; - -export type ObservabilityRuleFieldMap = typeof observabilityRuleFieldMap; diff --git a/x-pack/plugins/observability/common/utils/formatters/duration.ts b/x-pack/plugins/observability/common/utils/formatters/duration.ts index 6bbeb44ef06a..481005332cc3 100644 --- a/x-pack/plugins/observability/common/utils/formatters/duration.ts +++ b/x-pack/plugins/observability/common/utils/formatters/duration.ts @@ -201,6 +201,9 @@ export function asDuration( const formatter = getDurationFormatter(value); return formatter(value, { defaultValue, extended }).formatted; } + +export type AsDuration = typeof asDuration; + /** * Convert a microsecond value to decimal milliseconds. Normally we use * `asDuration`, but this is used in places like tables where we always want diff --git a/x-pack/plugins/observability/common/utils/formatters/formatters.ts b/x-pack/plugins/observability/common/utils/formatters/formatters.ts index 3c307f64fa0a..9bdccc7e9edf 100644 --- a/x-pack/plugins/observability/common/utils/formatters/formatters.ts +++ b/x-pack/plugins/observability/common/utils/formatters/formatters.ts @@ -47,6 +47,8 @@ export function asPercent( return numeral(decimal).format('0.0%'); } +export type AsPercent = typeof asPercent; + export function asDecimalOrInteger(value: number) { // exact 0 or above 10 should not have decimal if (value === 0 || value >= 10) { diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 0ee978c75d6c..52d5493ae69a 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -15,7 +15,8 @@ "requiredPlugins": [ "data", "alerting", - "ruleRegistry" + "ruleRegistry", + "triggersActionsUi" ], "ui": true, "server": true, diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index c0b51652a7d0..9182a0e8196c 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { Observable } from 'rxjs'; import { AppMountParameters, CoreStart } from 'src/core/public'; import { ObservabilityPublicPluginsStart } from '../plugin'; -import { createObservabilityRuleRegistryMock } from '../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock'; import { renderApp } from './'; describe('renderApp', () => { @@ -58,7 +58,7 @@ describe('renderApp', () => { core, plugins, appMountParameters: params, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), }); unmount(); }).not.toThrowError(); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 8607b57b4266..460aa6c35bdb 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -18,11 +18,12 @@ import { import { PluginContext } from '../context/plugin_context'; import { usePluginContext } from '../hooks/use_plugin_context'; import { useRouteParams } from '../hooks/use_route_params'; -import { ObservabilityPublicPluginsStart, ObservabilityRuleRegistry } from '../plugin'; +import { ObservabilityPublicPluginsStart } from '../plugin'; import { HasDataContextProvider } from '../context/has_data_context'; import { Breadcrumbs, routes } from '../routes'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { ConfigSchema } from '..'; +import { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry'; function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumbs) { return breadcrumbs.map(({ text }) => text).reverse(); @@ -72,12 +73,12 @@ export const renderApp = ({ core, plugins, appMountParameters, - observabilityRuleRegistry, + observabilityRuleTypeRegistry, }: { config: ConfigSchema; core: CoreStart; plugins: ObservabilityPublicPluginsStart; - observabilityRuleRegistry: ObservabilityRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; appMountParameters: AppMountParameters; }) => { const { element, history } = appMountParameters; @@ -94,7 +95,7 @@ export const renderApp = ({ ReactDOM.render( diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index d41f131ef521..e2669d87d677 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -14,7 +14,8 @@ import * as hasDataHook from '../../../../hooks/use_has_data'; import * as pluginContext from '../../../../hooks/use_plugin_context'; import { HasDataContextValue } from '../../../../context/has_data_context'; import { AppMountParameters, CoreStart } from 'kibana/public'; -import { ObservabilityPublicPluginsStart, ObservabilityRuleRegistry } from '../../../../plugin'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -41,10 +42,7 @@ describe('APMSection', () => { } as unknown) as CoreStart, appMountParameters: {} as AppMountParameters, config: { unsafe: { alertingExperience: { enabled: true } } }, - observabilityRuleRegistry: ({ - registerType: jest.fn(), - getTypeByRuleId: jest.fn(), - } as unknown) as ObservabilityRuleRegistry, + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), plugins: ({ data: { query: { diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index fa4d1a744e3e..b4227cc122dd 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -7,7 +7,6 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; -import { createObservabilityRuleRegistryMock } from '../../../../rules/observability_rule_registry_mock'; import { HasDataContextValue } from '../../../../context/has_data_context'; import * as fetcherHook from '../../../../hooks/use_fetcher'; import * as hasDataHook from '../../../../hooks/use_has_data'; @@ -16,6 +15,7 @@ import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { render } from '../../../../utils/test_helper'; import { UXSection } from './'; import { response } from './mock_data/ux.mock'; +import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -55,7 +55,7 @@ describe('UXSection', () => { }, }, } as unknown) as ObservabilityPublicPluginsStart, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), })); }); it('renders with core web vitals', () => { diff --git a/x-pack/plugins/observability/public/context/plugin_context.tsx b/x-pack/plugins/observability/public/context/plugin_context.tsx index eea259b36d5b..9b0bacc4c117 100644 --- a/x-pack/plugins/observability/public/context/plugin_context.tsx +++ b/x-pack/plugins/observability/public/context/plugin_context.tsx @@ -7,15 +7,16 @@ import { createContext } from 'react'; import { AppMountParameters, CoreStart } from 'kibana/public'; -import { ObservabilityPublicPluginsStart, ObservabilityRuleRegistry } from '../plugin'; +import { ObservabilityPublicPluginsStart } from '../plugin'; import { ConfigSchema } from '..'; +import { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry'; export interface PluginContextValue { appMountParameters: AppMountParameters; config: ConfigSchema; core: CoreStart; plugins: ObservabilityPublicPluginsStart; - observabilityRuleRegistry: ObservabilityRuleRegistry; + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; } export const PluginContext = createContext({} as PluginContextValue); diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts index 43fbc18062b7..8808d6390e36 100644 --- a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts +++ b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts @@ -10,7 +10,7 @@ import * as pluginContext from './use_plugin_context'; import { AppMountParameters, CoreStart } from 'kibana/public'; import { ObservabilityPublicPluginsStart } from '../plugin'; import * as kibanaUISettings from './use_kibana_ui_settings'; -import { createObservabilityRuleRegistryMock } from '../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -39,7 +39,7 @@ describe('useTimeRange', () => { }, }, } as unknown) as ObservabilityPublicPluginsStart, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), })); jest.spyOn(kibanaUISettings, 'useKibanaUISettings').mockImplementation(() => ({ from: '2020-10-08T05:00:00.000Z', @@ -81,7 +81,7 @@ describe('useTimeRange', () => { }, }, } as unknown) as ObservabilityPublicPluginsStart, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), })); }); it('returns ranges and absolute times from kibana default settings', () => { diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index a011d1fc2c41..8dd2f6a57eef 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -62,4 +62,5 @@ export { getApmTraceUrl } from './utils/get_apm_trace_url'; export { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils'; export type { SeriesUrl } from './components/shared/exploratory_view/types'; -export { FormatterRuleRegistry } from './rules/formatter_rule_registry'; +export type { 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/pages/alerts/alerts.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx index 6940f6aaad69..0d47f3da89d3 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.stories.tsx @@ -13,7 +13,7 @@ import { AlertsPage } from '.'; import { HttpSetup } from '../../../../../../src/core/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { PluginContext, PluginContextValue } from '../../context/plugin_context'; -import { createObservabilityRuleRegistryMock } from '../../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; import { createCallObservabilityApi } from '../../services/call_observability_api'; import type { ObservabilityAPIReturnType } from '../../services/call_observability_api/types'; import { apmAlertResponseExample, dynamicIndexPattern } from './example_data'; @@ -62,7 +62,7 @@ export default { core: { http: { basePath: { prepend: (_: string) => '' } }, }, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), } as unknown) as PluginContextValue } > diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx index 96d3c1fc9c39..90c75a70c081 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/alerts_flyout.stories.tsx @@ -62,25 +62,26 @@ Example.args = { reason: 'Error count for opbeans-java was above the threshold', active: true, start: 1618235449493, - - 'rule.id': 'apm.error_rate', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'rule.name': 'Error count threshold | opbeans-java (smith test)', - 'kibana.rac.alert.duration.us': 61787000, - 'kibana.observability.evaluation.threshold': 0, - 'kibana.rac.alert.status': 'open', - tags: ['apm', 'service.name:opbeans-java'], - 'kibana.rac.alert.uuid': 'c50fbc70-0d77-462d-ac0a-f2bd0b8512e4', - 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81', - 'event.action': 'active', - '@timestamp': '2021-04-14T21:43:42.966Z', - 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production', - 'processor.event': 'error', - 'kibana.rac.alert.start': '2021-04-14T21:42:41.179Z', - 'kibana.rac.producer': 'apm', - 'event.kind': 'state', - 'rule.category': 'Error count threshold', - 'kibana.observability.evaluation.value': 1, + fields: { + 'rule.id': 'apm.error_rate', + 'service.environment': ['production'], + 'service.name': ['opbeans-java'], + 'rule.name': 'Error count threshold | opbeans-java (smith test)', + 'kibana.rac.alert.duration.us': 61787000, + 'kibana.rac.alert.evaluation.threshold': 0, + 'kibana.rac.alert.status': 'open', + tags: ['apm', 'service.name:opbeans-java'], + 'kibana.rac.alert.uuid': 'c50fbc70-0d77-462d-ac0a-f2bd0b8512e4', + 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81', + 'event.action': 'active', + '@timestamp': '2021-04-14T21:43:42.966Z', + 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production', + 'processor.event': ['error'], + 'kibana.rac.alert.start': '2021-04-14T21:42:41.179Z', + 'kibana.rac.producer': 'apm', + 'event.kind': 'state', + 'rule.category': 'Error count threshold', + 'kibana.rac.alert.evaluation.value': 1, + }, }, } as Args; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 09fe464aa8cb..b4bf96bcc690 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -22,6 +22,14 @@ import { import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; +import { + ALERT_DURATION, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_SEVERITY_LEVEL, + RULE_CATEGORY, + RULE_NAME, +} from '@kbn/rule-data-utils/target/technical_field_names'; import { TopAlert } from '../'; import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; import { asDuration } from '../../../../common/utils/formatters'; @@ -46,7 +54,7 @@ export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) { title: i18n.translate('xpack.observability.alertsFlyout.severityLabel', { defaultMessage: 'Severity', }), - description: , + description: , }, { title: i18n.translate('xpack.observability.alertsFlyout.triggeredLabel', { @@ -60,25 +68,25 @@ export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) { title: i18n.translate('xpack.observability.alertsFlyout.durationLabel', { defaultMessage: 'Duration', }), - description: asDuration(alert['kibana.rac.alert.duration.us'], { extended: true }), + description: asDuration(alert.fields[ALERT_DURATION], { extended: true }), }, { title: i18n.translate('xpack.observability.alertsFlyout.expectedValueLabel', { defaultMessage: 'Expected value', }), - description: alert['kibana.observability.evaluation.threshold'] ?? '-', + description: alert.fields[ALERT_EVALUATION_THRESHOLD] ?? '-', }, { title: i18n.translate('xpack.observability.alertsFlyout.actualValueLabel', { defaultMessage: 'Actual value', }), - description: alert['kibana.observability.evaluation.value'] ?? '-', + description: alert.fields[ALERT_EVALUATION_VALUE] ?? '-', }, { title: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', { defaultMessage: 'Rule type', }), - description: alert['rule.category'] ?? '-', + description: alert.fields[RULE_CATEGORY] ?? '-', }, ]; @@ -86,7 +94,7 @@ export function AlertsFlyout({ onClose, alert }: AlertsFlyoutProps) { -

{alert['rule.name']}

+

{alert.fields[RULE_NAME]}

{alert.reason} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx index b0ff156fde37..f377186623a0 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx @@ -16,6 +16,10 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; +import { + ALERT_DURATION, + ALERT_SEVERITY_LEVEL, +} from '@kbn/rule-data-utils/target/technical_field_names'; import { asDuration } from '../../../common/utils/formatters'; import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -94,9 +98,7 @@ export function AlertsTable(props: AlertsTableProps) { }), render: (_, alert) => { const { active } = alert; - return active - ? null - : asDuration(alert['kibana.rac.alert.duration.us'], { extended: true }); + return active ? null : asDuration(alert.fields[ALERT_DURATION], { extended: true }); }, }, { @@ -105,7 +107,7 @@ export function AlertsTable(props: AlertsTableProps) { defaultMessage: 'Severity', }), render: (_, alert) => { - return ; + return ; }, }, { diff --git a/x-pack/plugins/observability/public/pages/alerts/example_data.ts b/x-pack/plugins/observability/public/pages/alerts/example_data.ts index dba6f1e9aaa2..5318fce82c1d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/example_data.ts +++ b/x-pack/plugins/observability/public/pages/alerts/example_data.ts @@ -7,42 +7,42 @@ export const apmAlertResponseExample = [ { - 'rule.id': 'apm.error_rate', - 'service.name': 'opbeans-java', - 'rule.name': 'Error count threshold | opbeans-java (smith test)', - 'kibana.rac.alert.duration.us': 180057000, - 'kibana.rac.alert.status': 'open', - 'kibana.rac.alert.severity.level': 'warning', + 'rule.id': ['apm.error_rate'], + 'service.name': ['opbeans-java'], + 'rule.name': ['Error count threshold | opbeans-java (smith test)'], + 'kibana.rac.alert.duration.us': [180057000], + 'kibana.rac.alert.status': ['open'], + 'kibana.rac.alert.severity.level': ['warning'], tags: ['apm', 'service.name:opbeans-java'], - 'kibana.rac.alert.uuid': '0175ec0a-a3b1-4d41-b557-e21c2d024352', - 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81', - 'event.action': 'active', - '@timestamp': '2021-04-12T13:53:49.550Z', - 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production', - 'kibana.rac.alert.start': '2021-04-12T13:50:49.493Z', - 'kibana.rac.producer': 'apm', - 'event.kind': 'state', - 'rule.category': 'Error count threshold', + 'kibana.rac.alert.uuid': ['0175ec0a-a3b1-4d41-b557-e21c2d024352'], + 'rule.uuid': ['474920d0-93e9-11eb-ac86-0b455460de81'], + 'event.action': ['active'], + '@timestamp': ['2021-04-12T13:53:49.550Z'], + 'kibana.rac.alert.id': ['apm.error_rate_opbeans-java_production'], + 'kibana.rac.alert.start': ['2021-04-12T13:50:49.493Z'], + 'kibana.rac.producer': ['apm'], + 'event.kind': ['state'], + 'rule.category': ['Error count threshold'], 'service.environment': ['production'], 'processor.event': ['error'], }, { - 'rule.id': 'apm.error_rate', - 'service.name': 'opbeans-java', - 'rule.name': 'Error count threshold | opbeans-java (smith test)', - 'kibana.rac.alert.duration.us': 2419005000, - 'kibana.rac.alert.end': '2021-04-12T13:49:49.446Z', - 'kibana.rac.alert.status': 'closed', + 'rule.id': ['apm.error_rate'], + 'service.name': ['opbeans-java'], + 'rule.name': ['Error count threshold | opbeans-java (smith test)'], + 'kibana.rac.alert.duration.us': [2419005000], + 'kibana.rac.alert.end': ['2021-04-12T13:49:49.446Z'], + 'kibana.rac.alert.status': ['closed'], tags: ['apm', 'service.name:opbeans-java'], - 'kibana.rac.alert.uuid': '32b940e1-3809-4c12-8eee-f027cbb385e2', - 'rule.uuid': '474920d0-93e9-11eb-ac86-0b455460de81', - 'event.action': 'close', - '@timestamp': '2021-04-12T13:49:49.446Z', - 'kibana.rac.alert.id': 'apm.error_rate_opbeans-java_production', - 'kibana.rac.alert.start': '2021-04-12T13:09:30.441Z', - 'kibana.rac.producer': 'apm', - 'event.kind': 'state', - 'rule.category': 'Error count threshold', + 'kibana.rac.alert.uuid': ['32b940e1-3809-4c12-8eee-f027cbb385e2'], + 'rule.uuid': ['474920d0-93e9-11eb-ac86-0b455460de81'], + 'event.action': ['close'], + '@timestamp': ['2021-04-12T13:49:49.446Z'], + 'kibana.rac.alert.id': ['apm.error_rate_opbeans-java_production'], + 'kibana.rac.alert.start': ['2021-04-12T13:09:30.441Z'], + 'kibana.rac.producer': ['apm'], + 'event.kind': ['state'], + 'rule.category': ['Error count threshold'], 'service.environment': ['production'], 'processor.event': ['error'], }, diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index a6d5c6926973..1f468a70d097 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -17,6 +17,16 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { format, parse } from 'url'; +import { + ALERT_START, + EVENT_ACTION, + RULE_ID, + RULE_NAME, +} from '@kbn/rule-data-utils/target/technical_field_names'; +import { + ParsedTechnicalFields, + parseTechnicalFields, +} from '../../../../rule_registry/common/parse_technical_fields'; import { asDuration, asPercent } from '../../../common/utils/formatters'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { useFetcher } from '../../hooks/use_fetcher'; @@ -30,7 +40,8 @@ import { AlertsTable } from './alerts_table'; export type TopAlertResponse = ObservabilityAPIReturnType<'GET /api/observability/rules/alerts/top'>[number]; -export interface TopAlert extends TopAlertResponse { +export interface TopAlert { + fields: ParsedTechnicalFields; start: number; reason: string; link?: string; @@ -42,7 +53,7 @@ interface AlertsPageProps { } export function AlertsPage({ routeParams }: AlertsPageProps) { - const { core, observabilityRuleRegistry } = usePluginContext(); + const { core, observabilityRuleTypeRegistry } = usePluginContext(); const { prepend } = core.http.basePath; const history = useHistory(); const { @@ -74,18 +85,19 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { }, }).then((alerts) => { return alerts.map((alert) => { - const ruleType = observabilityRuleRegistry.getTypeByRuleId(alert['rule.id']); + const parsedFields = parseTechnicalFields(alert); + const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[RULE_ID]!); const formatted = { link: undefined, - reason: alert['rule.name'], - ...(ruleType?.format?.({ alert, formatters: { asDuration, asPercent } }) ?? {}), + reason: parsedFields[RULE_NAME]!, + ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), }; const parsedLink = formatted.link ? parse(formatted.link, true) : undefined; return { - ...alert, ...formatted, + fields: parsedFields, link: parsedLink ? format({ ...parsedLink, @@ -96,13 +108,13 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { }, }) : undefined, - active: alert['event.action'] !== 'close', - start: new Date(alert['kibana.rac.alert.start']).getTime(), + active: parsedFields[EVENT_ACTION] !== 'close', + start: new Date(parsedFields[ALERT_START]!).getTime(), }; }); }); }, - [kuery, observabilityRuleRegistry, rangeFrom, rangeTo] + [kuery, observabilityRuleTypeRegistry, rangeFrom, rangeTo] ); return ( diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index 559aa8d5884a..ebd1c7385916 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -23,7 +23,7 @@ import { emptyResponse as emptyLogsResponse, fetchLogsData } from './mock/logs.m import { emptyResponse as emptyMetricsResponse, fetchMetricsData } from './mock/metrics.mock'; import { newsFeedFetchData } from './mock/news_feed.mock'; import { emptyResponse as emptyUptimeResponse, fetchUptimeData } from './mock/uptime.mock'; -import { createObservabilityRuleRegistryMock } from '../../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; function unregisterAll() { unregisterDataHandler({ appName: 'apm' }); @@ -54,7 +54,7 @@ const withCore = makeDecorator({ }, }, } as unknown) as ObservabilityPublicPluginsStart, - observabilityRuleRegistry: createObservabilityRuleRegistryMock(), + observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), }} > diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 517675fe1d52..6856bc97b4a3 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -7,6 +7,10 @@ import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, +} from '../../triggers_actions_ui/public'; import { AppMountParameters, AppUpdater, @@ -25,26 +29,23 @@ import type { HomePublicPluginStart, } from '../../../../src/plugins/home/public'; import type { LensPublicStart } from '../../lens/public'; -import type { RuleRegistryPublicPluginSetupContract } from '../../rule_registry/public'; -import type { ObservabilityRuleFieldMap } from '../common/rules/observability_rule_field_map'; -import { observabilityRuleRegistrySettings } from '../common/rules/observability_rule_registry_settings'; import { registerDataHandler } from './data_handler'; -import { FormatterRuleRegistry } from './rules/formatter_rule_registry'; import { createCallObservabilityApi } from './services/call_observability_api'; import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; import { ConfigSchema } from '.'; +import { createObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry'; export type ObservabilityPublicSetup = ReturnType; -export type ObservabilityRuleRegistry = ObservabilityPublicSetup['ruleRegistry']; export interface ObservabilityPublicPluginsSetup { data: DataPublicPluginSetup; - ruleRegistry: RuleRegistryPublicPluginSetupContract; + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; home?: HomePublicPluginSetup; } export interface ObservabilityPublicPluginsStart { home?: HomePublicPluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; lens: LensPublicStart; } @@ -75,11 +76,9 @@ export class Plugin createCallObservabilityApi(coreSetup.http); - const observabilityRuleRegistry = pluginsSetup.ruleRegistry.registry.create({ - ...observabilityRuleRegistrySettings, - fieldMap: {} as ObservabilityRuleFieldMap, - ctor: FormatterRuleRegistry, - }); + const observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistry( + pluginsSetup.triggersActionsUi.alertTypeRegistry + ); const mount = async (params: AppMountParameters) => { // Load application bundle @@ -92,7 +91,7 @@ export class Plugin core: coreStart, plugins: pluginsStart, appMountParameters: params, - observabilityRuleRegistry, + observabilityRuleTypeRegistry, }); }; @@ -165,7 +164,7 @@ export class Plugin return { dashboard: { register: registerDataHandler }, - ruleRegistry: observabilityRuleRegistry, + observabilityRuleTypeRegistry, isAlertingExperienceEnabled: () => config.unsafe.alertingExperience.enabled, }; } 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 new file mode 100644 index 000000000000..cba9df83c6fe --- /dev/null +++ b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts @@ -0,0 +1,31 @@ +/* + * 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 { AlertTypeModel, AlertTypeRegistryContract } from '../../../triggers_actions_ui/public'; +import { ParsedTechnicalFields } from '../../../rule_registry/common/parse_technical_fields'; +import { AsDuration, AsPercent } from '../../common/utils/formatters'; + +type Formatter = (options: { + fields: ParsedTechnicalFields & Record; + formatters: { asDuration: AsDuration; asPercent: AsPercent }; +}) => { reason: string; link: string }; + +export function createObservabilityRuleTypeRegistry(alertTypeRegistry: AlertTypeRegistryContract) { + const formatters: Array<{ typeId: string; fn: Formatter }> = []; + return { + register: (type: AlertTypeModel & { format: Formatter }) => { + const { format, ...rest } = type; + formatters.push({ typeId: type.id, fn: format }); + alertTypeRegistry.register(rest); + }, + getFormatter: (typeId: string) => { + return formatters.find((formatter) => formatter.typeId === typeId)?.fn; + }, + }; +} + +export type ObservabilityRuleTypeRegistry = ReturnType; diff --git a/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts b/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts deleted file mode 100644 index 0d0d22cf750f..000000000000 --- a/x-pack/plugins/observability/public/rules/formatter_rule_registry.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { RuleType } from '../../../rule_registry/public'; -import type { BaseRuleFieldMap, OutputOfFieldMap } from '../../../rule_registry/common'; -import { RuleRegistry } from '../../../rule_registry/public'; -import type { asDuration, asPercent } from '../../common/utils/formatters'; - -type AlertTypeOf = OutputOfFieldMap; - -type FormattableRuleType = RuleType & { - format?: (options: { - alert: AlertTypeOf; - formatters: { - asDuration: typeof asDuration; - asPercent: typeof asPercent; - }; - }) => { - reason?: string; - link?: string; - }; -}; - -export class FormatterRuleRegistry extends RuleRegistry< - TFieldMap, - FormattableRuleType -> {} diff --git a/x-pack/plugins/observability/public/rules/observability_rule_registry_mock.ts b/x-pack/plugins/observability/public/rules/observability_rule_registry_mock.ts deleted file mode 100644 index 389b581b5fb6..000000000000 --- a/x-pack/plugins/observability/public/rules/observability_rule_registry_mock.ts +++ /dev/null @@ -1,17 +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 { ObservabilityRuleRegistry } from '../plugin'; - -const createRuleRegistryMock = () => ({ - registerType: () => {}, - getTypeByRuleId: () => ({ format: () => ({ link: '/test/example' }) }), - create: () => createRuleRegistryMock(), -}); - -export const createObservabilityRuleRegistryMock = () => - createRuleRegistryMock() as ObservabilityRuleRegistry & ReturnType; diff --git a/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts b/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts new file mode 100644 index 000000000000..b2cf48f8e1c3 --- /dev/null +++ b/x-pack/plugins/observability/public/rules/observability_rule_type_registry_mock.ts @@ -0,0 +1,16 @@ +/* + * 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 { ObservabilityRuleTypeRegistry } from './create_observability_rule_type_registry'; + +const createRuleTypeRegistryMock = () => ({ + registerFormatter: () => {}, +}); + +export const createObservabilityRuleTypeRegistryMock = () => + createRuleTypeRegistryMock() as ObservabilityRuleTypeRegistry & + ReturnType; diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx index 63e34b018aed..ef7c62a143f2 100644 --- a/x-pack/plugins/observability/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -15,7 +15,7 @@ import translations from '../../../translations/translations/ja-JP.json'; import { PluginContext } from '../context/plugin_context'; import { ObservabilityPublicPluginsStart } from '../plugin'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { createObservabilityRuleRegistryMock } from '../rules/observability_rule_registry_mock'; +import { createObservabilityRuleTypeRegistryMock } from '../rules/observability_rule_type_registry_mock'; const appMountParameters = ({ setHeaderActionMenu: () => {} } as unknown) as AppMountParameters; @@ -37,14 +37,14 @@ const plugins = ({ data: { query: { timefilter: { timefilter: { setTime: jest.fn() } } } }, } as unknown) as ObservabilityPublicPluginsStart; -const observabilityRuleRegistry = createObservabilityRuleRegistryMock(); +const observabilityRuleTypeRegistry = createObservabilityRuleTypeRegistryMock(); export const render = (component: React.ReactNode) => { return testLibRender( {component} diff --git a/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts b/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts index 0045c0f0c675..ddfc112ab145 100644 --- a/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts +++ b/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts @@ -4,24 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Required } from 'utility-types'; -import { ObservabilityRuleRegistryClient } from '../../types'; +import { ALERT_UUID, TIMESTAMP } from '@kbn/rule-data-utils/target/technical_field_names'; +import { RuleDataClient } from '../../../../rule_registry/server'; import { kqlQuery, rangeQuery } from '../../utils/queries'; export async function getTopAlerts({ - ruleRegistryClient, + ruleDataClient, start, end, kuery, size, }: { - ruleRegistryClient: ObservabilityRuleRegistryClient; + ruleDataClient: RuleDataClient; start: number; end: number; kuery?: string; size: number; }) { - const response = await ruleRegistryClient.search({ + const response = await ruleDataClient.getReader().search({ body: { query: { bool: { @@ -30,26 +30,18 @@ export async function getTopAlerts({ }, fields: ['*'], collapse: { - field: 'kibana.rac.alert.uuid', + field: ALERT_UUID, }, size, sort: { - '@timestamp': 'desc', + [TIMESTAMP]: 'desc', }, _source: false, }, + allow_no_indices: true, }); - return response.events.map((event) => { - return event as Required< - typeof event, - | 'rule.id' - | 'rule.name' - | 'kibana.rac.alert.start' - | 'event.action' - | 'rule.category' - | 'rule.name' - | 'kibana.rac.alert.duration.us' - >; + return response.hits.hits.map((hit) => { + return hit.fields; }); } diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index b5208260297d..046a9a62d5fa 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -6,6 +6,7 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { RuleDataClient } from '../../rule_registry/server'; import { ObservabilityConfig } from '.'; import { bootstrapAnnotations, @@ -16,11 +17,8 @@ import type { RuleRegistryPluginSetupContract } from '../../rule_registry/server import { uiSettings } from './ui_settings'; import { registerRoutes } from './routes/register_routes'; import { getGlobalObservabilityServerRouteRepository } from './routes/get_global_observability_server_route_repository'; -import { observabilityRuleRegistrySettings } from '../common/rules/observability_rule_registry_settings'; -import { observabilityRuleFieldMap } from '../common/rules/observability_rule_field_map'; export type ObservabilityPluginSetup = ReturnType; -export type ObservabilityRuleRegistry = ObservabilityPluginSetup['ruleRegistry']; export class ObservabilityPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { @@ -51,19 +49,25 @@ export class ObservabilityPlugin implements Plugin { }); } - const observabilityRuleRegistry = plugins.ruleRegistry.create({ - ...observabilityRuleRegistrySettings, - fieldMap: observabilityRuleFieldMap, + const start = () => core.getStartServices().then(([coreStart]) => coreStart); + + const ruleDataClient = new RuleDataClient({ + getClusterClient: async () => { + const coreStart = await start(); + return coreStart.elasticsearch.client.asInternalUser; + }, + ready: () => Promise.resolve(), + alias: plugins.ruleRegistry.getFullAssetName(), }); registerRoutes({ core: { setup: core, - start: () => core.getStartServices().then(([coreStart]) => coreStart), + start, }, - ruleRegistry: observabilityRuleRegistry, logger: this.initContext.logger.get(), repository: getGlobalObservabilityServerRouteRepository(), + ruleDataClient, }); return { @@ -71,7 +75,6 @@ export class ObservabilityPlugin implements Plugin { const api = await annotationsApiPromise; return api?.getScopedAnnotationsClient(...args); }, - ruleRegistry: observabilityRuleRegistry, }; } diff --git a/x-pack/plugins/observability/server/routes/register_routes.ts b/x-pack/plugins/observability/server/routes/register_routes.ts index 85ee456b812b..75b6703cc64d 100644 --- a/x-pack/plugins/observability/server/routes/register_routes.ts +++ b/x-pack/plugins/observability/server/routes/register_routes.ts @@ -13,23 +13,23 @@ import { import { CoreSetup, CoreStart, Logger, RouteRegistrar } from 'kibana/server'; import Boom from '@hapi/boom'; import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors'; -import { ObservabilityRuleRegistry } from '../plugin'; +import { RuleDataClient } from '../../../rule_registry/server'; import { ObservabilityRequestHandlerContext } from '../types'; import { AbstractObservabilityServerRouteRepository } from './types'; export function registerRoutes({ - ruleRegistry, repository, core, logger, + ruleDataClient, }: { core: { setup: CoreSetup; start: () => Promise; }; - ruleRegistry: ObservabilityRuleRegistry; repository: AbstractObservabilityServerRouteRepository; logger: Logger; + ruleDataClient: RuleDataClient; }) { const routes = repository.getRoutes(); @@ -59,10 +59,10 @@ export function registerRoutes({ const data = (await handler({ context, request, - ruleRegistry, core, logger, params: decodedParams, + ruleDataClient, })) as any; return response.ok({ body: data }); diff --git a/x-pack/plugins/observability/server/routes/rules.ts b/x-pack/plugins/observability/server/routes/rules.ts index cd3f4976e0af..1f500adff5dc 100644 --- a/x-pack/plugins/observability/server/routes/rules.ts +++ b/x-pack/plugins/observability/server/routes/rules.ts @@ -6,7 +6,6 @@ */ import * as t from 'io-ts'; import { isoToEpochRt, toNumberRt } from '@kbn/io-ts-utils'; -import Boom from '@hapi/boom'; import { createObservabilityServerRoute } from './create_observability_server_route'; import { createObservabilityServerRouteRepository } from './create_observability_server_route_repository'; import { getTopAlerts } from '../lib/rules/get_top_alerts'; @@ -28,22 +27,13 @@ const alertsListRoute = createObservabilityServerRoute({ }), ]), }), - handler: async ({ ruleRegistry, context, params }) => { - const ruleRegistryClient = await ruleRegistry.createScopedRuleRegistryClient({ - context, - alertsClient: context.alerting.getAlertsClient(), - }); - - if (!ruleRegistryClient) { - throw Boom.failedDependency('xpack.ruleRegistry.unsafe.write.enabled is set to false'); - } - + handler: async ({ ruleDataClient, context, params }) => { const { query: { start, end, kuery, size = 100 }, } = params; return getTopAlerts({ - ruleRegistryClient, + ruleDataClient, start, end, kuery, @@ -57,17 +47,10 @@ const alertsDynamicIndexPatternRoute = createObservabilityServerRoute({ options: { tags: [], }, - handler: async ({ ruleRegistry, context }) => { - const ruleRegistryClient = await ruleRegistry.createScopedRuleRegistryClient({ - context, - alertsClient: context.alerting.getAlertsClient(), - }); - - if (!ruleRegistryClient) { - throw Boom.failedDependency(); - } + handler: async ({ ruleDataClient }) => { + const reader = ruleDataClient.getReader({ namespace: 'observability' }); - return ruleRegistryClient.getDynamicIndexPattern(); + return reader.getDynamicIndexPattern(); }, }); diff --git a/x-pack/plugins/observability/server/routes/types.ts b/x-pack/plugins/observability/server/routes/types.ts index 0588bf8df229..1fa7229c6cf6 100644 --- a/x-pack/plugins/observability/server/routes/types.ts +++ b/x-pack/plugins/observability/server/routes/types.ts @@ -12,7 +12,7 @@ import type { ServerRouteRepository, } from '@kbn/server-route-repository'; import { CoreSetup, CoreStart, KibanaRequest, Logger } from 'kibana/server'; -import { ObservabilityRuleRegistry } from '../plugin'; +import { RuleDataClient } from '../../../rule_registry/server'; import { ObservabilityServerRouteRepository } from './get_global_observability_server_route_repository'; import { ObservabilityRequestHandlerContext } from '../types'; @@ -24,7 +24,7 @@ export interface ObservabilityRouteHandlerResources { start: () => Promise; setup: CoreSetup; }; - ruleRegistry: ObservabilityRuleRegistry; + ruleDataClient: RuleDataClient; request: KibanaRequest; context: ObservabilityRequestHandlerContext; logger: Logger; diff --git a/x-pack/plugins/observability/server/types.ts b/x-pack/plugins/observability/server/types.ts index 81b32b3f8db7..da13e60804a6 100644 --- a/x-pack/plugins/observability/server/types.ts +++ b/x-pack/plugins/observability/server/types.ts @@ -7,9 +7,7 @@ import type { IRouter, RequestHandlerContext } from 'src/core/server'; import type { AlertingApiRequestHandlerContext } from '../../alerting/server'; -import type { ScopedRuleRegistryClient, FieldMapOf } from '../../rule_registry/server'; import type { LicensingApiRequestHandlerContext } from '../../licensing/server'; -import type { ObservabilityRuleRegistry } from './plugin'; export type { ObservabilityRouteCreateOptions, @@ -31,7 +29,3 @@ export interface ObservabilityRequestHandlerContext extends RequestHandlerContex * @internal */ export type ObservabilityPluginRouter = IRouter; - -export type ObservabilityRuleRegistryClient = ScopedRuleRegistryClient< - FieldMapOf ->; diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index 2c8f534a63d6..cfbde612b45a 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -2,62 +2,129 @@ The rule registry plugin aims to make it easy for rule type producers to have their rules produce the data that they need to build rich experiences on top of a unified experience, without the risk of mapping conflicts. -A rule registry creates a template, an ILM policy, and an alias. The template mappings can be configured. It also injects a client scoped to these indices. +The plugin installs default component templates and a default lifecycle policy that rule type producers can use to create index templates. -It also supports inheritance, which means that producers can create a registry specific to their solution or rule type, and specify additional mappings to be used. +It also exposes a rule data client that will create or update the index stream that rules will write data to. It will not do so on plugin setup or start, but only when data is written. -The rule registry plugin creates a root rule registry, with the mappings defined needed to create a unified experience. Rule type producers can use the plugin to access the root rule registry, and create their own registry that branches off of the root rule registry. The rule registry client sees data from its own registry, and all registries that branches off of it. It does not see data from its parents. +## Configuration -## Enabling writing - -Set +By default, these indices will be prefixed with `.alerts`. To change this, for instance to support legacy multitenancy, set the following configuration option: ```yaml -xpack.ruleRegistry.unsafe.write.enabled: true +xpack.ruleRegistry.index: '.kibana-alerts' ``` -in your Kibana configuration to allow the Rule Registry to write events to the alert indices. +To disable writing entirely: + +```yaml +xpack.ruleRegistry.write.enabled: false +``` -## Creating a rule registry +## Setting up the index template -To create a rule registry, producers should add the `ruleRegistry` plugin to their dependencies. They can then use the `ruleRegistry.create` method to create a child registry, with the additional mappings that should be used by specifying `fieldMap`: +On plugin setup, rule type producers can create the index template as follows: ```ts -const observabilityRegistry = plugins.ruleRegistry.create({ - name: 'observability', - fieldMap: { - ...pickWithPatterns(ecsFieldMap, 'host.name', 'service.name'), - }, -}); -``` +// get the FQN of the component template. All assets are prefixed with the configured `index` value, which is `.alerts` by default. -`fieldMap` is a key-value map of field names and mapping options: +const componentTemplateName = plugins.ruleRegistry.getFullAssetName( + 'apm-mappings' +); -```ts -{ - '@timestamp': { - type: 'date', - array: false, - required: true, - } +// if write is disabled, don't install these templates +if (!plugins.ruleRegistry.isWriteEnabled()) { + return; } -``` -ECS mappings are generated via a script in the rule registry plugin directory. These mappings are available in x-pack/plugins/rule_registry/server/generated/ecs_field_map.ts. - -To pick many fields, you can use `pickWithPatterns`, which supports wildcards with full type support. +// create or update the component template that should be used +await plugins.ruleRegistry.createOrUpdateComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + // mappingFromFieldMap is a utility function that will generate an + // ES mapping from a field map object. You can also define a literal + // mapping. + mappings: mappingFromFieldMap({ + [SERVICE_NAME]: { + type: 'keyword', + }, + [SERVICE_ENVIRONMENT]: { + type: 'keyword', + }, + [TRANSACTION_TYPE]: { + type: 'keyword', + }, + [PROCESSOR_EVENT]: { + type: 'keyword', + }, + }), + }, + }, +}); -If a registry is created, it will initialise as soon as the core services needed become available. It will create a (versioned) template, alias, and ILM policy, but only if these do not exist yet. +// Install the index template, that is composed of the component template +// defined above, and others. It is important that the technical component +// template is included. This will ensure functional compatibility across +// rule types, for a future scenario where a user will want to "point" the +// data from a rule to a different index. +await plugins.ruleRegistry.createOrUpdateIndexTemplate({ + name: plugins.ruleRegistry.getFullAssetName('apm-index-template'), + body: { + index_patterns: [ + plugins.ruleRegistry.getFullAssetName('observability-apm*'), + ], + composed_of: [ + // Technical component template, required + plugins.ruleRegistry.getFullAssetName( + TECHNICAL_COMPONENT_TEMPLATE_NAME + ), + componentTemplateName, + ], + }, +}); -## Rule registry client +// Finally, create the rule data client that can be injected into rule type +// executors and API endpoints +const ruleDataClient = new RuleDataClient({ + alias: plugins.ruleRegistry.getFullAssetName('observability-apm'), + getClusterClient: async () => { + const coreStart = await getCoreStart(); + return coreStart.elasticsearch.client.asInternalUser; + }, + ready, +}); -The rule registry client can either be injected in the executor, or created in the scope of a request. It exposes a `search` method and a `bulkIndex` method. When `search` is called, it first gets all the rules the current user has access to, and adds these ids to the search request that it executes. This means that the user can only see data from rules they have access to. +// to start writing data, call `getWriter().bulk()`. It supports a `namespace` +// property as well, that for instance can be used to write data to a space-specific +// index. +await ruleDataClient.getWriter().bulk({ + body: eventsToIndex.flatMap((event) => [{ index: {} }, event]), +}); -Both `search` and `bulkIndex` are fully typed, in the sense that they reflect the mappings defined for the registry. +// to read data, simply call ruleDataClient.getReader().search: +const response = await ruleDataClient.getReader().search({ + body: { + query: { + }, + size: 100, + fields: ['*'], + collapse: { + field: ALERT_UUID, + }, + sort: { + '@timestamp': 'desc', + }, + }, + allow_no_indices: true, +}); +``` ## Schema -The following fields are available in the root rule registry: +The following fields are defined in the technical field component template and should always be used: - `@timestamp`: the ISO timestamp of the alert event. For the lifecycle rule type helper, it is always the value of `startedAt` that is injected by the Kibana alerting framework. - `event.kind`: signal (for the changeable alert document), state (for the state changes of the alert, e.g. when it opens, recovers, or changes in severity), or metric (individual evaluations that might be related to an alert). @@ -67,7 +134,7 @@ The following fields are available in the root rule registry: - `rule.uuid`: the saved objects id of the rule. - `rule.name`: the name of the rule (as specified by the user). - `rule.category`: the name of the rule type (as defined by the rule type producer) -- `kibana.rac.producer`: the producer of the rule type. Usually a Kibana plugin. e.g., `APM`. +- `kibana.rac.alert.producer`: the producer of the rule type. Usually a Kibana plugin. e.g., `APM`. - `kibana.rac.alert.id`: the id of the alert, that is unique within the context of the rule execution it was created in. E.g., for a rule that monitors latency for all services in all environments, this might be `opbeans-java:production`. - `kibana.rac.alert.uuid`: the unique identifier for the alert during its lifespan. If an alert recovers (or closes), this identifier is re-generated when it is opened again. - `kibana.rac.alert.status`: the status of the alert. Can be `open` or `closed`. @@ -76,5 +143,5 @@ The following fields are available in the root rule registry: - `kibana.rac.alert.duration.us`: the duration of the alert, in microseconds. This is always the difference between either the current time, or the time when the alert recovered. - `kibana.rac.alert.severity.level`: the severity of the alert, as a keyword (e.g. critical). - `kibana.rac.alert.severity.value`: the severity of the alert, as a numerical value, which allows sorting. - -This list is not final - just a start. Field names might change or moved to a scoped registry. If we implement log and sequence based rule types the list of fields will grow. If a rule type needs additional fields, the recommendation would be to have the field in its own registry first (or in its producer’s registry), and if usage is more broadly adopted, it can be moved to the root registry. +- `kibana.rac.alert.evaluation.value`: The measured (numerical value). +- `kibana.rac.alert.threshold.value`: The threshold that was defined (or, in case of multiple thresholds, the one that was exceeded). diff --git a/x-pack/plugins/apm/server/lib/alerts/create_apm_lifecycle_rule_type.ts b/x-pack/plugins/rule_registry/common/assets.ts similarity index 53% rename from x-pack/plugins/apm/server/lib/alerts/create_apm_lifecycle_rule_type.ts rename to x-pack/plugins/rule_registry/common/assets.ts index 8d250a5765cc..1a5b14c605ea 100644 --- a/x-pack/plugins/apm/server/lib/alerts/create_apm_lifecycle_rule_type.ts +++ b/x-pack/plugins/rule_registry/common/assets.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; -import { APMRuleRegistry } from '../../plugin'; - -export const createAPMLifecycleRuleType = createLifecycleRuleTypeFactory(); +export const TECHNICAL_COMPONENT_TEMPLATE_NAME = `technical-mappings`; +export const ECS_COMPONENT_TEMPLATE_NAME = `ecs-mappings`; +export const DEFAULT_ILM_POLICY_ID = 'ilm-policy'; diff --git a/x-pack/plugins/rule_registry/common/assets/component_templates/ecs_component_template.ts b/x-pack/plugins/rule_registry/common/assets/component_templates/ecs_component_template.ts new file mode 100644 index 000000000000..7acbe0bc1227 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets/component_templates/ecs_component_template.ts @@ -0,0 +1,24 @@ +/* + * 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 { merge } from 'lodash'; +import { mappingFromFieldMap } from '../../mapping_from_field_map'; +import { ClusterPutComponentTemplateBody } from '../../types'; +import { ecsFieldMap } from '../field_maps/ecs_field_map'; +import { technicalRuleFieldMap } from '../field_maps/technical_rule_field_map'; + +export const ecsComponentTemplate: ClusterPutComponentTemplateBody = { + template: { + settings: { + number_of_shards: 1, + }, + mappings: merge( + {}, + mappingFromFieldMap(ecsFieldMap), + mappingFromFieldMap(technicalRuleFieldMap) + ), + }, +}; diff --git a/x-pack/plugins/rule_registry/common/assets/component_templates/technical_component_template.ts b/x-pack/plugins/rule_registry/common/assets/component_templates/technical_component_template.ts new file mode 100644 index 000000000000..cc096faba387 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets/component_templates/technical_component_template.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 { mappingFromFieldMap } from '../../mapping_from_field_map'; +import { ClusterPutComponentTemplateBody } from '../../types'; +import { technicalRuleFieldMap } from '../field_maps/technical_rule_field_map'; + +export const technicalComponentTemplate: ClusterPutComponentTemplateBody = { + template: { + settings: { + number_of_shards: 1, + }, + mappings: mappingFromFieldMap(technicalRuleFieldMap), + }, +}; diff --git a/x-pack/plugins/rule_registry/common/field_map/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts similarity index 100% rename from x-pack/plugins/rule_registry/common/field_map/ecs_field_map.ts rename to x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts 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 new file mode 100644 index 000000000000..a946e9523548 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -0,0 +1,56 @@ +/* + * 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 { pickWithPatterns } from '../../../common/pick_with_patterns'; +import { + ALERT_DURATION, + ALERT_END, + ALERT_EVALUATION_THRESHOLD, + ALERT_EVALUATION_VALUE, + ALERT_ID, + ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY_VALUE, + ALERT_START, + ALERT_STATUS, + ALERT_UUID, + EVENT_ACTION, + EVENT_KIND, + PRODUCER, + RULE_CATEGORY, + RULE_ID, + RULE_NAME, + RULE_UUID, + TAGS, + TIMESTAMP, +} from '../../../common/technical_rule_data_field_names'; +import { ecsFieldMap } from './ecs_field_map'; + +export const technicalRuleFieldMap = { + ...pickWithPatterns( + ecsFieldMap, + TIMESTAMP, + EVENT_KIND, + EVENT_ACTION, + RULE_UUID, + RULE_ID, + RULE_NAME, + RULE_CATEGORY, + TAGS + ), + [PRODUCER]: { type: 'keyword' }, + [ALERT_UUID]: { type: 'keyword' }, + [ALERT_ID]: { type: 'keyword' }, + [ALERT_START]: { type: 'date' }, + [ALERT_END]: { type: 'date' }, + [ALERT_DURATION]: { type: 'long' }, + [ALERT_SEVERITY_LEVEL]: { type: 'keyword' }, + [ALERT_SEVERITY_VALUE]: { type: 'long' }, + [ALERT_STATUS]: { type: 'keyword' }, + [ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 }, + [ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 }, +} as const; + +export type TechnicalRuleFieldMaps = typeof technicalRuleFieldMap; diff --git a/x-pack/plugins/rule_registry/common/assets/index_templates/base_index_template.ts b/x-pack/plugins/rule_registry/common/assets/index_templates/base_index_template.ts new file mode 100644 index 000000000000..ee2e45640c14 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/assets/index_templates/base_index_template.ts @@ -0,0 +1,15 @@ +/* + * 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 const baseIndexTemplate = { + template: { + settings: { + number_of_shards: 1, + number_of_replicas: 0, + }, + }, +}; diff --git a/x-pack/plugins/rule_registry/server/rule_registry/defaults/ilm_policy.ts b/x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts similarity index 86% rename from x-pack/plugins/rule_registry/server/rule_registry/defaults/ilm_policy.ts rename to x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts index c80f7e772f30..f207087f7aa1 100644 --- a/x-pack/plugins/rule_registry/server/rule_registry/defaults/ilm_policy.ts +++ b/x-pack/plugins/rule_registry/common/assets/lifecycle_policies/default_lifecycle_policy.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { ILMPolicy } from '../types'; - -export const defaultIlmPolicy: ILMPolicy = { +export const defaultLifecyclePolicy = { policy: { phases: { hot: { diff --git a/x-pack/plugins/rule_registry/common/field_map/base_rule_field_map.ts b/x-pack/plugins/rule_registry/common/field_map/base_rule_field_map.ts deleted file mode 100644 index 22a74212d2ce..000000000000 --- a/x-pack/plugins/rule_registry/common/field_map/base_rule_field_map.ts +++ /dev/null @@ -1,33 +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 { ecsFieldMap } from './ecs_field_map'; -import { pickWithPatterns } from '../pick_with_patterns'; - -export const baseRuleFieldMap = { - ...pickWithPatterns( - ecsFieldMap, - '@timestamp', - 'event.kind', - 'event.action', - 'rule.uuid', - 'rule.id', - 'rule.name', - 'rule.category', - 'tags' - ), - 'kibana.rac.producer': { type: 'keyword' }, - 'kibana.rac.alert.uuid': { type: 'keyword' }, - 'kibana.rac.alert.id': { type: 'keyword' }, - 'kibana.rac.alert.start': { type: 'date' }, - 'kibana.rac.alert.end': { type: 'date' }, - 'kibana.rac.alert.duration.us': { type: 'long' }, - 'kibana.rac.alert.severity.level': { type: 'keyword' }, - 'kibana.rac.alert.severity.value': { type: 'long' }, - 'kibana.rac.alert.status': { type: 'keyword' }, -} as const; - -export type BaseRuleFieldMap = typeof baseRuleFieldMap; diff --git a/x-pack/plugins/rule_registry/common/field_map/index.ts b/x-pack/plugins/rule_registry/common/field_map/index.ts index 8db5c2738439..fac8575b8af4 100644 --- a/x-pack/plugins/rule_registry/common/field_map/index.ts +++ b/x-pack/plugins/rule_registry/common/field_map/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -export * from './base_rule_field_map'; -export * from './ecs_field_map'; export * from './merge_field_maps'; export * from './runtime_type_from_fieldmap'; export * from './types'; diff --git a/x-pack/plugins/rule_registry/common/index.ts b/x-pack/plugins/rule_registry/common/index.ts index b614feebc974..5d36cd8cad7b 100644 --- a/x-pack/plugins/rule_registry/common/index.ts +++ b/x-pack/plugins/rule_registry/common/index.ts @@ -4,5 +4,4 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -export * from './field_map'; -export * from './pick_with_patterns'; +export { parseTechnicalFields } from './parse_technical_fields'; diff --git a/x-pack/plugins/rule_registry/server/rule_registry/field_map/mapping_from_field_map.ts b/x-pack/plugins/rule_registry/common/mapping_from_field_map.ts similarity index 79% rename from x-pack/plugins/rule_registry/server/rule_registry/field_map/mapping_from_field_map.ts rename to x-pack/plugins/rule_registry/common/mapping_from_field_map.ts index f1d712690643..17eb5ae8967a 100644 --- a/x-pack/plugins/rule_registry/server/rule_registry/field_map/mapping_from_field_map.ts +++ b/x-pack/plugins/rule_registry/common/mapping_from_field_map.ts @@ -5,11 +5,11 @@ * 2.0. */ +import { TypeMapping } from '@elastic/elasticsearch/api/types'; import { set } from '@elastic/safer-lodash-set'; -import { FieldMap } from '../../../common'; -import { Mappings } from '../types'; +import { FieldMap } from './field_map/types'; -export function mappingFromFieldMap(fieldMap: FieldMap): Mappings { +export function mappingFromFieldMap(fieldMap: FieldMap): TypeMapping { const mappings = { dynamic: 'strict' as const, properties: {}, diff --git a/x-pack/plugins/rule_registry/common/parse_technical_fields.ts b/x-pack/plugins/rule_registry/common/parse_technical_fields.ts new file mode 100644 index 000000000000..9d92c657468a --- /dev/null +++ b/x-pack/plugins/rule_registry/common/parse_technical_fields.ts @@ -0,0 +1,25 @@ +/* + * 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 { isLeft } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { technicalRuleFieldMap } from './assets/field_maps/technical_rule_field_map'; +import { runtimeTypeFromFieldMap } from './field_map'; + +const technicalFieldRuntimeType = runtimeTypeFromFieldMap(technicalRuleFieldMap); + +export const parseTechnicalFields = (input: unknown) => { + const validate = technicalFieldRuntimeType.decode(input); + + if (isLeft(validate)) { + throw new Error(PathReporter.report(validate).join('\n')); + } + + return technicalFieldRuntimeType.encode(validate.right); +}; + +export type ParsedTechnicalFields = ReturnType; diff --git a/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts new file mode 100644 index 000000000000..5c954a31e79a --- /dev/null +++ b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts @@ -0,0 +1,8 @@ +/* + * 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 '@kbn/rule-data-utils/target/technical_field_names'; diff --git a/x-pack/plugins/rule_registry/common/types.ts b/x-pack/plugins/rule_registry/common/types.ts new file mode 100644 index 000000000000..299d2c300ab4 --- /dev/null +++ b/x-pack/plugins/rule_registry/common/types.ts @@ -0,0 +1,21 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; + +export type PutIndexTemplateRequest = estypes.PutIndexTemplateRequest & { + body?: { composed_of?: string[] }; +}; + +export interface ClusterPutComponentTemplateBody { + template: { + settings: { + number_of_shards: number; + }; + mappings: estypes.TypeMapping; + }; +} diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index ec2b366f739e..7e3f8bf6afb7 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -10,6 +10,5 @@ "alerting", "triggersActionsUi" ], - "server": true, - "ui": true + "server": true } diff --git a/x-pack/plugins/rule_registry/public/index.ts b/x-pack/plugins/rule_registry/public/index.ts deleted file mode 100644 index 59697261ff20..000000000000 --- a/x-pack/plugins/rule_registry/public/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { PluginInitializerContext } from 'kibana/public'; -import { Plugin } from './plugin'; - -export type { RuleRegistryPublicPluginSetupContract } from './plugin'; -export { RuleRegistry } from './rule_registry'; -export type { IRuleRegistry, RuleType } from './rule_registry/types'; - -export const plugin = (context: PluginInitializerContext) => { - return new Plugin(context); -}; diff --git a/x-pack/plugins/rule_registry/public/plugin.ts b/x-pack/plugins/rule_registry/public/plugin.ts deleted file mode 100644 index 7f0bceefb679..000000000000 --- a/x-pack/plugins/rule_registry/public/plugin.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - CoreSetup, - CoreStart, - Plugin as PluginClass, - PluginInitializerContext, -} from '../../../../src/core/public'; -import type { - PluginSetupContract as AlertingPluginPublicSetupContract, - PluginStartContract as AlertingPluginPublicStartContract, -} from '../../alerting/public'; -import type { - TriggersAndActionsUIPublicPluginSetup, - TriggersAndActionsUIPublicPluginStart, -} from '../../triggers_actions_ui/public'; -import type { BaseRuleFieldMap } from '../common'; -import { RuleRegistry } from './rule_registry'; - -interface RuleRegistrySetupPlugins { - alerting: AlertingPluginPublicSetupContract; - triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; -} - -interface RuleRegistryStartPlugins { - alerting: AlertingPluginPublicStartContract; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; -} - -export type RuleRegistryPublicPluginSetupContract = ReturnType; - -export class Plugin - implements PluginClass { - constructor(context: PluginInitializerContext) {} - - public setup(core: CoreSetup, plugins: RuleRegistrySetupPlugins) { - const rootRegistry = new RuleRegistry({ - fieldMap: {} as BaseRuleFieldMap, - alertTypeRegistry: plugins.triggersActionsUi.alertTypeRegistry, - }); - return { - registry: rootRegistry, - }; - } - - start(core: CoreStart, plugins: RuleRegistryStartPlugins) { - return { - registerType: plugins.triggersActionsUi.alertTypeRegistry, - }; - } -} diff --git a/x-pack/plugins/rule_registry/public/rule_registry/index.ts b/x-pack/plugins/rule_registry/public/rule_registry/index.ts deleted file mode 100644 index ea47fe2e26aa..000000000000 --- a/x-pack/plugins/rule_registry/public/rule_registry/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { BaseRuleFieldMap } from '../../common'; -import type { RuleType, CreateRuleRegistry, RuleRegistryConstructorOptions } from './types'; - -export class RuleRegistry { - protected types: TRuleType[] = []; - - constructor(private readonly options: RuleRegistryConstructorOptions) {} - - getTypes(): TRuleType[] { - return this.types; - } - - getTypeByRuleId(id: string): TRuleType | undefined { - return this.types.find((type) => type.id === id); - } - - registerType(type: TRuleType) { - this.types.push(type); - if (this.options.parent) { - this.options.parent.registerType(type); - } else { - this.options.alertTypeRegistry.register(type); - } - } - - create: CreateRuleRegistry = ({ fieldMap, ctor }) => { - const createOptions = { - fieldMap: { - ...this.options.fieldMap, - ...fieldMap, - }, - alertTypeRegistry: this.options.alertTypeRegistry, - parent: this, - }; - - const registry = ctor ? new ctor(createOptions) : new RuleRegistry(createOptions); - - return registry as any; - }; -} diff --git a/x-pack/plugins/rule_registry/public/rule_registry/types.ts b/x-pack/plugins/rule_registry/public/rule_registry/types.ts deleted file mode 100644 index 7c186385ebd3..000000000000 --- a/x-pack/plugins/rule_registry/public/rule_registry/types.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import type { AlertTypeRegistryContract } from '../../../triggers_actions_ui/public'; -import type { BaseRuleFieldMap, FieldMap } from '../../common'; - -export interface RuleRegistryConstructorOptions { - fieldMap: TFieldMap; - alertTypeRegistry: AlertTypeRegistryContract; - parent?: IRuleRegistry; -} - -export type RuleType = Parameters[0]; - -export type RegisterRuleType< - TFieldMap extends BaseRuleFieldMap, - TAdditionalRegisterOptions = {} -> = (type: RuleType & TAdditionalRegisterOptions) => void; - -export type RuleRegistryExtensions = Record< - T, - (...args: any[]) => any ->; - -export type CreateRuleRegistry< - TFieldMap extends BaseRuleFieldMap, - TRuleType extends RuleType, - TInstanceType = undefined -> = < - TNextFieldMap extends FieldMap, - TRuleRegistryInstance extends IRuleRegistry< - TFieldMap & TNextFieldMap, - any - > = TInstanceType extends IRuleRegistry - ? TInstanceType - : IRuleRegistry ->(options: { - fieldMap: TNextFieldMap; - ctor?: new ( - options: RuleRegistryConstructorOptions - ) => TRuleRegistryInstance; -}) => TRuleRegistryInstance; - -export interface IRuleRegistry< - TFieldMap extends BaseRuleFieldMap, - TRuleType extends RuleType, - TInstanceType = undefined -> { - create: CreateRuleRegistry; - registerType(type: TRuleType): void; - getTypeByRuleId(ruleId: string): TRuleType; - getTypes(): TRuleType[]; -} - -export type FieldMapOfRuleRegistry = TRuleRegistry extends IRuleRegistry< - infer TFieldMap, - any -> - ? TFieldMap - : never; diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index 9fd1408fcdb2..b51ba3e10f91 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -9,21 +9,23 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { RuleRegistryPlugin } from './plugin'; -export { RuleRegistryPluginSetupContract } from './plugin'; -export { createLifecycleRuleTypeFactory } from './rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory'; -export { FieldMapOf } from './types'; -export { ScopedRuleRegistryClient } from './rule_registry/create_scoped_rule_registry_client/types'; +export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin'; +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 const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), - unsafe: schema.object({ - write: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), + write: schema.object({ + enabled: schema.boolean({ defaultValue: true }), }), + index: schema.string({ defaultValue: '.alerts' }), }), }; -export type RuleRegistryConfig = TypeOf; +export type RuleRegistryPluginConfig = TypeOf; export const plugin = (initContext: PluginInitializerContext) => new RuleRegistryPlugin(initContext); diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 09df47c40a39..3c645f98f5c7 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -6,44 +6,44 @@ */ import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; -import { PluginSetupContract as AlertingPluginSetupContract } from '../../alerting/server'; -import { RuleRegistry } from './rule_registry'; -import { defaultIlmPolicy } from './rule_registry/defaults/ilm_policy'; -import { BaseRuleFieldMap, baseRuleFieldMap } from '../common'; -import { RuleRegistryConfig } from '.'; +import { RuleDataPluginService } from './rule_data_plugin_service'; +import { RuleRegistryPluginConfig } from '.'; -export type RuleRegistryPluginSetupContract = RuleRegistry; +export type RuleRegistryPluginSetupContract = RuleDataPluginService; +export type RuleRegistryPluginStartContract = void; export class RuleRegistryPlugin implements Plugin { constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; } - public setup( - core: CoreSetup, - plugins: { alerting: AlertingPluginSetupContract } - ): RuleRegistryPluginSetupContract { - const globalConfig = this.initContext.config.legacy.get(); - const config = this.initContext.config.get(); + public setup(core: CoreSetup): RuleRegistryPluginSetupContract { + const config = this.initContext.config.get(); const logger = this.initContext.logger.get(); - const rootRegistry = new RuleRegistry({ - coreSetup: core, - ilmPolicy: defaultIlmPolicy, - fieldMap: baseRuleFieldMap, - kibanaIndex: globalConfig.kibana.index, - name: 'alerts', - kibanaVersion: this.initContext.env.packageInfo.version, - logger: logger.get('root'), - alertingPluginSetupContract: plugins.alerting, - writeEnabled: config.unsafe.write.enabled, + const service = new RuleDataPluginService({ + logger, + isWriteEnabled: config.write.enabled, + index: config.index, + getClusterClient: async () => { + const [coreStart] = await core.getStartServices(); + + return coreStart.elasticsearch.client.asInternalUser; + }, + }); + + service.init().catch((originalError) => { + const error = new Error('Failed installing assets'); + // @ts-ignore + error.stack = originalError.stack; + logger.error(error); }); - return rootRegistry; + return service; } - public start() {} + public start(): RuleRegistryPluginStartContract {} public stop() {} } diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/index.ts b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts new file mode 100644 index 000000000000..135c870f2072 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_client/index.ts @@ -0,0 +1,132 @@ +/* + * 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 { TypeMapping } from '@elastic/elasticsearch/api/types'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { IndexPatternsFetcher } from '../../../../../src/plugins/data/server'; +import { + IRuleDataClient, + RuleDataClientConstructorOptions, + RuleDataReader, + RuleDataWriter, +} from './types'; + +function getNamespacedAlias(options: { alias: string; namespace?: string }) { + return [options.alias, options.namespace].filter(Boolean).join('-'); +} + +export class RuleDataClient implements IRuleDataClient { + constructor(private readonly options: RuleDataClientConstructorOptions) {} + + private async getClusterClient() { + await this.options.ready(); + return await this.options.getClusterClient(); + } + + getReader(options: { namespace?: string } = {}): RuleDataReader { + const index = `${[this.options.alias, options.namespace].filter(Boolean).join('-')}*`; + + return { + search: async (request) => { + const clusterClient = await this.getClusterClient(); + + const { body } = (await clusterClient.search({ + ...request, + index, + })) as { body: any }; + + return body; + }, + getDynamicIndexPattern: async () => { + const clusterClient = await this.getClusterClient(); + const indexPatternsFetcher = new IndexPatternsFetcher(clusterClient); + + const fields = await indexPatternsFetcher.getFieldsForWildcard({ + pattern: index, + }); + + return { + fields, + timeFieldName: '@timestamp', + title: index, + }; + }, + }; + } + + getWriter(options: { namespace?: string } = {}): RuleDataWriter { + const { namespace } = options; + const alias = getNamespacedAlias({ alias: this.options.alias, namespace }); + return { + bulk: async (request) => { + const clusterClient = await this.getClusterClient(); + + const requestWithDefaultParameters = { + ...request, + require_alias: true, + index: alias, + }; + + return clusterClient.bulk(requestWithDefaultParameters).then((response) => { + if (response.body.errors) { + if ( + response.body.items.length === 1 && + response.body.items[0]?.index?.error?.type === 'index_not_found_exception' + ) { + return this.createOrUpdateWriteTarget({ namespace }).then(() => { + return clusterClient.bulk(requestWithDefaultParameters); + }); + } + const error = new ResponseError(response); + throw error; + } + return response; + }); + }, + }; + } + + async createOrUpdateWriteTarget({ namespace }: { namespace?: string }) { + const alias = getNamespacedAlias({ alias: this.options.alias, namespace }); + + const clusterClient = await this.getClusterClient(); + + const { body: aliasExists } = await clusterClient.indices.existsAlias({ + name: alias, + }); + + const concreteIndexName = `${alias}-000001`; + + if (!aliasExists) { + try { + await clusterClient.indices.create({ + index: concreteIndexName, + body: { + aliases: { + [alias]: { + is_write_index: true, + }, + }, + }, + }); + } catch (err) { + // something might have created the index already, that sounds OK + if (err?.meta?.body?.type !== 'resource_already_exists_exception') { + throw err; + } + } + } + + const { body: simulateResponse } = await clusterClient.transport.request({ + method: 'POST', + path: `/_index_template/_simulate_index/${concreteIndexName}`, + }); + + const mappings: TypeMapping = simulateResponse.template.mappings; + + await clusterClient.indices.putMapping({ index: `${alias}*`, body: mappings }); + } +} diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts new file mode 100644 index 000000000000..348fca6a5818 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -0,0 +1,44 @@ +/* + * 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 { ApiResponse } from '@elastic/elasticsearch'; +import { BulkRequest, BulkResponse } from '@elastic/elasticsearch/api/types'; +import { ElasticsearchClient } from 'kibana/server'; +import { FieldDescriptor } from 'src/plugins/data/server'; +import { ESSearchRequest, ESSearchResponse } from 'typings/elasticsearch'; +import { TechnicalRuleDataFieldName } from '../../common/technical_rule_data_field_names'; + +export interface RuleDataReader { + search( + request: TSearchRequest + ): Promise< + ESSearchResponse>, TSearchRequest> + >; + getDynamicIndexPattern( + target?: string + ): Promise<{ + title: string; + timeFieldName: string; + fields: FieldDescriptor[]; + }>; +} + +export interface RuleDataWriter { + bulk(request: BulkRequest): Promise>; +} + +export interface IRuleDataClient { + getReader(options?: { namespace?: string }): RuleDataReader; + getWriter(options?: { namespace?: string }): RuleDataWriter; + createOrUpdateWriteTarget(options: { namespace?: string }): Promise; +} + +export interface RuleDataClientConstructorOptions { + getClusterClient: () => Promise; + ready: () => Promise; + alias: string; +} diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts new file mode 100644 index 000000000000..159e9b815259 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -0,0 +1,158 @@ +/* + * 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 { ClusterPutComponentTemplate } from '@elastic/elasticsearch/api/requestParams'; +import { estypes } from '@elastic/elasticsearch'; +import { ElasticsearchClient, Logger } from 'kibana/server'; +import { technicalComponentTemplate } from '../../common/assets/component_templates/technical_component_template'; +import { + DEFAULT_ILM_POLICY_ID, + ECS_COMPONENT_TEMPLATE_NAME, + TECHNICAL_COMPONENT_TEMPLATE_NAME, +} from '../../common/assets'; +import { ecsComponentTemplate } from '../../common/assets/component_templates/ecs_component_template'; +import { defaultLifecyclePolicy } from '../../common/assets/lifecycle_policies/default_lifecycle_policy'; +import { ClusterPutComponentTemplateBody, PutIndexTemplateRequest } from '../../common/types'; + +const BOOTSTRAP_TIMEOUT = 60000; + +interface RuleDataPluginServiceConstructorOptions { + getClusterClient: () => Promise; + logger: Logger; + isWriteEnabled: boolean; + index: string; +} + +function createSignal() { + let resolver: () => void; + + let ready: boolean = false; + + const promise = new Promise((resolve) => { + resolver = resolve; + }); + + function wait(): Promise { + return promise.then(() => { + ready = true; + }); + } + + function complete() { + resolver(); + } + + return { wait, complete, isReady: () => ready }; +} + +export class RuleDataPluginService { + signal = createSignal(); + + constructor(private readonly options: RuleDataPluginServiceConstructorOptions) {} + + private assertWriteEnabled() { + if (!this.isWriteEnabled) { + throw new Error('Write operations are disabled'); + } + } + + private async getClusterClient() { + return await this.options.getClusterClient(); + } + + async init() { + if (!this.isWriteEnabled) { + this.options.logger.info('Write is disabled, not installing assets'); + this.signal.complete(); + return; + } + + this.options.logger.info(`Installing assets in namespace ${this.getFullAssetName()}`); + + await this._createOrUpdateLifecyclePolicy({ + policy: this.getFullAssetName(DEFAULT_ILM_POLICY_ID), + body: defaultLifecyclePolicy, + }); + + await this._createOrUpdateComponentTemplate({ + name: this.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), + body: technicalComponentTemplate, + }); + + await this._createOrUpdateComponentTemplate({ + name: this.getFullAssetName(ECS_COMPONENT_TEMPLATE_NAME), + body: ecsComponentTemplate, + }); + + this.options.logger.info(`Installed all assets`); + + this.signal.complete(); + } + + private async _createOrUpdateComponentTemplate( + template: ClusterPutComponentTemplate + ) { + this.assertWriteEnabled(); + + const clusterClient = await this.getClusterClient(); + this.options.logger.debug(`Installing component template ${template.name}`); + return clusterClient.cluster.putComponentTemplate(template); + } + + private async _createOrUpdateIndexTemplate(template: PutIndexTemplateRequest) { + this.assertWriteEnabled(); + + const clusterClient = await this.getClusterClient(); + this.options.logger.debug(`Installing index template ${template.name}`); + return clusterClient.indices.putIndexTemplate(template); + } + + private async _createOrUpdateLifecyclePolicy(policy: estypes.PutLifecycleRequest) { + this.assertWriteEnabled(); + const clusterClient = await this.getClusterClient(); + + this.options.logger.debug(`Installing lifecycle policy ${policy.policy}`); + return clusterClient.ilm.putLifecycle(policy); + } + + async createOrUpdateComponentTemplate( + template: ClusterPutComponentTemplate + ) { + await this.wait(); + return this._createOrUpdateComponentTemplate(template); + } + + async createOrUpdateIndexTemplate(template: PutIndexTemplateRequest) { + await this.wait(); + return this._createOrUpdateIndexTemplate(template); + } + + async createOrUpdateLifecyclePolicy(policy: estypes.PutLifecycleRequest) { + await this.wait(); + return this._createOrUpdateLifecyclePolicy(policy); + } + + isReady() { + return this.signal.isReady(); + } + + wait() { + return Promise.race([ + this.signal.wait(), + new Promise((resolve, reject) => { + setTimeout(reject, BOOTSTRAP_TIMEOUT); + }), + ]); + } + + isWriteEnabled(): boolean { + return this.options.isWriteEnabled; + } + + getFullAssetName(assetName?: string) { + return [this.options.index, assetName].filter(Boolean).join('-'); + } +} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/index.ts b/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/index.ts deleted file mode 100644 index 0d7735380b64..000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/index.ts +++ /dev/null @@ -1,179 +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 { 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 { ClusterClientAdapter } from '../../../../event_log/server'; -import { TypeOfFieldMap } from '../../../common'; -import { ScopedRuleRegistryClient, EventsOf } from './types'; -import { BaseRuleFieldMap } from '../../../common'; -import { RuleRegistry } from '..'; - -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, - clusterClientAdapter, - indexAliasName, - indexTarget, - logger, - registry, - ruleData, -}: { - ruleUuids: string[]; - scopedClusterClient: ScopedClusterClient; - clusterClientAdapter: ClusterClientAdapter<{ - body: TypeOfFieldMap; - index: string; - }>; - indexAliasName: string; - indexTarget: string; - 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 response = await scopedClusterClient.asInternalUser.search({ - ...searchRequest, - index: indexTarget, - 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 indexPatternsFetcher = new IndexPatternsFetcher(scopedClusterClient.asInternalUser); - - const fields = await indexPatternsFetcher.getFieldsForWildcard({ - pattern: indexTarget, - }); - - return { - fields, - timeFieldName: '@timestamp', - title: indexTarget, - }; - }, - index: (doc) => { - const validation = fieldmapType.decode({ - ...doc, - ...defaults, - }); - - if (isLeft(validation)) { - throw createPathReporterError(validation); - } - - clusterClientAdapter.indexDocument({ - body: validation.right, - index: indexAliasName, - }); - }, - bulkIndex: (docs) => { - const validations = docs.map((doc) => { - return fieldmapType.decode({ - ...doc, - ...defaults, - }); - }); - - const errors = compact( - validations.map((validation) => - isLeft(validation) ? createPathReporterError(validation) : null - ) - ); - - errors.forEach((error) => { - logger.error(error); - }); - - const operations = compact( - validations.map((validation) => (isRight(validation) ? validation.right : null)) - ).map((doc) => ({ body: doc, index: indexAliasName })); - - return clusterClientAdapter.indexDocuments(operations); - }, - }; - - // @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/create_scoped_rule_registry_client/types.ts b/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/types.ts deleted file mode 100644 index f7b2394fe3a3..000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/create_scoped_rule_registry_client/types.ts +++ /dev/null @@ -1,57 +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 { FieldDescriptor } from 'src/plugins/data/server'; -import { ESSearchRequest, ESSearchResponse } from 'typings/elasticsearch'; -import { - PatternsUnionOf, - PickWithPatterns, - OutputOfFieldMap, - BaseRuleFieldMap, -} from '../../../common'; - -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(doc: Omit, PrepopulatedRuleEventFields>): void; - bulkIndex( - doc: Array, PrepopulatedRuleEventFields>> - ): Promise; -} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/index.ts b/x-pack/plugins/rule_registry/server/rule_registry/index.ts deleted file mode 100644 index bbc381f60a80..000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/index.ts +++ /dev/null @@ -1,328 +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 { CoreSetup, Logger, RequestHandlerContext } from 'kibana/server'; -import { inspect } from 'util'; -import { AlertsClient } from '../../../alerting/server'; -import { SpacesServiceStart } from '../../../spaces/server'; -import { - ActionVariable, - AlertInstanceState, - AlertTypeParams, - AlertTypeState, -} from '../../../alerting/common'; -import { createReadySignal, ClusterClientAdapter } from '../../../event_log/server'; -import { ILMPolicy } from './types'; -import { RuleParams, RuleType } from '../types'; -import { - mergeFieldMaps, - TypeOfFieldMap, - FieldMap, - FieldMapType, - BaseRuleFieldMap, - runtimeTypeFromFieldMap, -} from '../../common'; -import { mappingFromFieldMap } from './field_map/mapping_from_field_map'; -import { PluginSetupContract as AlertingPluginSetupContract } from '../../../alerting/server'; -import { createScopedRuleRegistryClient } from './create_scoped_rule_registry_client'; -import { ScopedRuleRegistryClient } from './create_scoped_rule_registry_client/types'; - -interface RuleRegistryOptions { - kibanaIndex: string; - kibanaVersion: string; - name: string; - logger: Logger; - coreSetup: CoreSetup; - spacesStart?: SpacesServiceStart; - fieldMap: TFieldMap; - ilmPolicy: ILMPolicy; - alertingPluginSetupContract: AlertingPluginSetupContract; - writeEnabled: boolean; -} - -export class RuleRegistry { - private readonly esAdapter: ClusterClientAdapter<{ - body: TypeOfFieldMap; - index: string; - }>; - private readonly children: Array> = []; - private readonly types: Array> = []; - - private readonly fieldmapType: FieldMapType; - - constructor(private readonly options: RuleRegistryOptions) { - const { logger, coreSetup } = options; - - this.fieldmapType = runtimeTypeFromFieldMap(options.fieldMap); - - const { wait, signal } = createReadySignal(); - - this.esAdapter = new ClusterClientAdapter<{ - body: TypeOfFieldMap; - index: string; - }>({ - wait, - elasticsearchClientPromise: coreSetup - .getStartServices() - .then(([{ elasticsearch }]) => elasticsearch.client.asInternalUser), - logger: logger.get('esAdapter'), - }); - - if (this.options.writeEnabled) { - this.initialize() - .then(() => { - this.options.logger.debug('Bootstrapped alerts index'); - signal(true); - }) - .catch((err) => { - logger.error(inspect(err, { depth: null })); - signal(false); - }); - } else { - logger.debug('Write disabled, indices are not being bootstrapped'); - } - } - - private getEsNames() { - const base = [this.options.kibanaIndex, this.options.name]; - const indexTarget = `${base.join('-')}*`; - const indexAliasName = [...base, this.options.kibanaVersion.toLowerCase()].join('-'); - const policyName = [...base, 'policy'].join('-'); - - return { - indexAliasName, - indexTarget, - policyName, - }; - } - - private async initialize() { - const { indexAliasName, policyName } = this.getEsNames(); - - const ilmPolicyExists = await this.esAdapter.doesIlmPolicyExist(policyName); - - if (!ilmPolicyExists) { - await this.esAdapter.createIlmPolicy( - policyName, - (this.options.ilmPolicy as unknown) as Record - ); - } - - const templateExists = await this.esAdapter.doesIndexTemplateExist(indexAliasName); - - const mappings = mappingFromFieldMap(this.options.fieldMap); - - const esClient = (await this.options.coreSetup.getStartServices())[0].elasticsearch.client - .asInternalUser; - - if (!templateExists) { - await this.esAdapter.createIndexTemplate(indexAliasName, { - index_patterns: [`${indexAliasName}-*`], - settings: { - number_of_shards: 1, - auto_expand_replicas: '0-1', - 'index.lifecycle.name': policyName, - 'index.lifecycle.rollover_alias': indexAliasName, - 'sort.field': '@timestamp', - 'sort.order': 'desc', - }, - mappings, - }); - } else { - await esClient.indices.putTemplate({ - name: indexAliasName, - body: { - index_patterns: [`${indexAliasName}-*`], - mappings, - }, - create: false, - }); - } - - const aliasExists = await this.esAdapter.doesAliasExist(indexAliasName); - - if (!aliasExists) { - await this.esAdapter.createIndex(`${indexAliasName}-000001`, { - aliases: { - [indexAliasName]: { - is_write_index: true, - }, - }, - }); - } else { - const { body: aliases } = (await esClient.indices.getAlias({ - index: indexAliasName, - })) as { body: Record }> }; - - const writeIndex = Object.entries(aliases).find( - ([indexName, alias]) => alias.aliases[indexAliasName]?.is_write_index === true - )![0]; - - const { body: fieldsInWriteIndex } = await esClient.fieldCaps({ - index: writeIndex, - fields: '*', - }); - - const fieldsNotOrDifferentInIndex = Object.entries(this.options.fieldMap).filter( - ([fieldName, descriptor]) => { - return ( - !fieldsInWriteIndex.fields[fieldName] || - !fieldsInWriteIndex.fields[fieldName][descriptor.type] - ); - } - ); - - if (fieldsNotOrDifferentInIndex.length > 0) { - this.options.logger.debug( - `Some fields were not found in write index mapping: ${Object.keys( - Object.fromEntries(fieldsNotOrDifferentInIndex) - ).join(',')}` - ); - this.options.logger.info(`Updating index mapping due to new fields`); - - await esClient.indices.putMapping({ - index: indexAliasName, - body: mappings, - }); - } - } - } - - getFieldMapType() { - return this.fieldmapType; - } - - getRuleTypeById(ruleTypeId: string) { - return this.types.find((type) => type.id === ruleTypeId); - } - - getRegistryByRuleTypeId(ruleTypeId: string): RuleRegistry | undefined { - if (this.getRuleTypeById(ruleTypeId)) { - return this; - } - - return this.children.find((child) => child.getRegistryByRuleTypeId(ruleTypeId)); - } - - async createScopedRuleRegistryClient({ - context, - alertsClient, - }: { - context: RequestHandlerContext; - alertsClient: AlertsClient; - }): Promise | undefined> { - if (!this.options.writeEnabled) { - return undefined; - } - const { indexAliasName, indexTarget } = this.getEsNames(); - - const frameworkAlerts = ( - await alertsClient.find({ - options: { - perPage: 1000, - }, - }) - ).data; - - return createScopedRuleRegistryClient({ - ruleUuids: frameworkAlerts.map((frameworkAlert) => frameworkAlert.id), - scopedClusterClient: context.core.elasticsearch.client, - clusterClientAdapter: this.esAdapter, - registry: this, - indexAliasName, - indexTarget, - logger: this.options.logger, - }); - } - - registerType( - type: RuleType - ) { - const logger = this.options.logger.get(type.id); - - const { indexAliasName, indexTarget } = this.getEsNames(); - - this.types.push(type); - - this.options.alertingPluginSetupContract.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({ - scopedClusterClient: services.scopedClusterClient, - ruleUuids: [rule.uuid], - clusterClientAdapter: this.esAdapter, - registry: this, - indexAliasName, - indexTarget, - ruleData: { - producer, - rule, - tags, - }, - logger: this.options.logger, - }), - } - : {}), - }, - }); - }, - }); - } - - create({ - name, - fieldMap, - ilmPolicy, - }: { - name: string; - fieldMap: TNextFieldMap; - ilmPolicy?: ILMPolicy; - }): RuleRegistry { - const mergedFieldMap = fieldMap - ? mergeFieldMaps(this.options.fieldMap, fieldMap) - : this.options.fieldMap; - - const child = new RuleRegistry({ - ...this.options, - logger: this.options.logger.get(name), - name: [this.options.name, name].filter(Boolean).join('-'), - fieldMap: mergedFieldMap, - ...(ilmPolicy ? { ilmPolicy } : {}), - }); - - this.children.push(child); - - // @ts-expect-error could be instantiated with a different subtype of constraint - return child; - } -} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory.ts deleted file mode 100644 index 65eaf0964cfc..000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/rule_type_helpers/create_lifecycle_rule_type_factory.ts +++ /dev/null @@ -1,235 +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 * 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 '../../types'; -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, - }; - }, - }; - }; -} diff --git a/x-pack/plugins/rule_registry/server/rule_registry/types.ts b/x-pack/plugins/rule_registry/server/rule_registry/types.ts deleted file mode 100644 index ec7293d1c1d4..000000000000 --- a/x-pack/plugins/rule_registry/server/rule_registry/types.ts +++ /dev/null @@ -1,42 +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. - */ - -export interface Mappings { - dynamic: 'strict' | boolean; - properties: Record; -} - -enum ILMPolicyPhase { - hot = 'hot', - delete = 'delete', -} - -enum ILMPolicyAction { - rollover = 'rollover', - delete = 'delete', -} - -interface ILMActionOptions { - [ILMPolicyAction.rollover]: { - max_size: string; - max_age: string; - }; - [ILMPolicyAction.delete]: {}; -} - -export interface ILMPolicy { - policy: { - phases: Record< - ILMPolicyPhase, - { - actions: { - [key in keyof ILMActionOptions]?: ILMActionOptions[key]; - }; - } - >; - }; -} diff --git a/x-pack/plugins/rule_registry/server/types.ts b/x-pack/plugins/rule_registry/server/types.ts index dd54046365d9..959c05fd1334 100644 --- a/x-pack/plugins/rule_registry/server/types.ts +++ b/x-pack/plugins/rule_registry/server/types.ts @@ -4,97 +4,37 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Type, TypeOf } from '@kbn/config-schema'; -import { Logger } from 'kibana/server'; + import { - ActionVariable, AlertInstanceContext, AlertInstanceState, AlertTypeParams, AlertTypeState, } from '../../alerting/common'; -import { ActionGroup, AlertExecutorOptions } from '../../alerting/server'; -import { RuleRegistry } from './rule_registry'; -import { ScopedRuleRegistryClient } from './rule_registry/create_scoped_rule_registry_client/types'; -import { BaseRuleFieldMap } from '../common'; - -export type RuleParams = Type; +import { AlertType } from '../../alerting/server'; -type TypeOfRuleParams = TypeOf; +type SimpleAlertType< + TParams extends AlertTypeParams = {}, + TAlertInstanceContext extends AlertInstanceContext = {} +> = AlertType; -type RuleExecutorServices< - TFieldMap extends BaseRuleFieldMap, - TActionVariable extends ActionVariable -> = AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - { [key in TActionVariable['name']]: any }, - string ->['services'] & { - logger: Logger; - scopedRuleRegistryClient?: ScopedRuleRegistryClient; -}; - -type PassthroughAlertExecutorOptions = Pick< - AlertExecutorOptions< - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, - string - >, - 'previousStartedAt' | 'startedAt' | 'state' ->; - -type RuleExecutorFunction< - TFieldMap extends BaseRuleFieldMap, - TRuleParams extends RuleParams, - TActionVariable extends ActionVariable, - TAdditionalRuleExecutorServices extends Record +export type AlertTypeExecutor< + TParams extends AlertTypeParams = {}, + TAlertInstanceContext extends AlertInstanceContext = {}, + TServices extends Record = {} > = ( - options: PassthroughAlertExecutorOptions & { - services: RuleExecutorServices & TAdditionalRuleExecutorServices; - params: TypeOfRuleParams; - rule: { - id: string; - uuid: string; - name: string; - category: string; - }; - producer: string; + options: Parameters['executor']>[0] & { + services: TServices; } -) => Promise>; - -interface RuleTypeBase { - id: string; - name: string; - actionGroups: Array>; - defaultActionGroupId: string; - producer: string; - minimumLicenseRequired: 'basic' | 'gold' | 'trial'; -} - -export type RuleType< - TFieldMap extends BaseRuleFieldMap, - TRuleParams extends RuleParams, - TActionVariable extends ActionVariable, - TAdditionalRuleExecutorServices extends Record = {} -> = RuleTypeBase & { - validate: { - params: TRuleParams; - }; - actionVariables: { - context: TActionVariable[]; - }; - executor: RuleExecutorFunction< - TFieldMap, - TRuleParams, - TActionVariable, - TAdditionalRuleExecutorServices - >; +) => Promise; + +export type AlertTypeWithExecutor< + TParams extends AlertTypeParams = {}, + TAlertInstanceContext extends AlertInstanceContext = {}, + TServices extends Record = {} +> = Omit< + AlertType, + 'executor' +> & { + executor: AlertTypeExecutor; }; - -export type FieldMapOf< - TRuleRegistry extends RuleRegistry -> = TRuleRegistry extends RuleRegistry ? TFieldMap : never; 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 new file mode 100644 index 000000000000..b523dd6770b9 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type_factory.ts @@ -0,0 +1,246 @@ +/* + * 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 { 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, +} from '../../../alerting/common'; +import { + ALERT_DURATION, + ALERT_END, + ALERT_ID, + ALERT_START, + ALERT_STATUS, + ALERT_UUID, + EVENT_ACTION, + EVENT_KIND, + RULE_UUID, + TIMESTAMP, +} from '../../common/technical_rule_data_field_names'; +import { AlertTypeWithExecutor } from '../types'; +import { ParsedTechnicalFields, parseTechnicalFields } from '../../common/parse_technical_fields'; +import { getRuleExecutorData } from './get_rule_executor_data'; + +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: { + ruleDataClient: RuleDataClient; + logger: Logger; +}) => < + TParams extends AlertTypeParams, + TAlertInstanceContext extends AlertInstanceContext, + TServices extends { alertWithLifecycle: LifecycleAlertService } +>( + type: AlertTypeWithExecutor +) => AlertTypeWithExecutor; + +export const createLifecycleRuleTypeFactory: CreateLifecycleRuleTypeFactory = ({ + logger, + ruleDataClient, +}) => (type) => { + return { + ...type, + executor: async (options) => { + const { + services: { alertInstanceFactory }, + state: previousState, + } = 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]: 'state', + [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) { + await ruleDataClient.getWriter().bulk({ + body: eventsToIndex.flatMap((event) => [{ index: {} }, 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: nextTrackedAlerts, + }; + }, + }; +}; 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 new file mode 100644 index 000000000000..1ea640add7b4 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts @@ -0,0 +1,39 @@ +/* + * 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 { + PRODUCER, + RULE_CATEGORY, + RULE_ID, + RULE_NAME, + RULE_UUID, + TAGS, +} from '../../common/technical_rule_data_field_names'; +import { AlertTypeExecutor, AlertTypeWithExecutor } from '../types'; + +export interface RuleExecutorData { + [RULE_CATEGORY]: string; + [RULE_ID]: string; + [RULE_UUID]: string; + [RULE_NAME]: string; + [PRODUCER]: string; + [TAGS]: string[]; +} + +export function getRuleExecutorData( + type: AlertTypeWithExecutor, + options: Parameters[0] +) { + return { + [RULE_ID]: type.id, + [RULE_UUID]: options.alertId, + [RULE_CATEGORY]: type.name, + [RULE_NAME]: options.name, + [TAGS]: options.tags, + [PRODUCER]: type.producer, + }; +} diff --git a/x-pack/plugins/rule_registry/server/utils/with_rule_data_client_factory.ts b/x-pack/plugins/rule_registry/server/utils/with_rule_data_client_factory.ts new file mode 100644 index 000000000000..02ff6b10f74c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/utils/with_rule_data_client_factory.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AlertInstanceContext, AlertTypeParams } from '../../../alerting/common'; +import { RuleDataClient } from '../rule_data_client'; +import { AlertTypeWithExecutor } from '../types'; + +export const withRuleDataClientFactory = (ruleDataClient: RuleDataClient) => < + TParams extends AlertTypeParams, + TAlertInstanceContext extends AlertInstanceContext, + TServices extends Record = {} +>( + type: AlertTypeWithExecutor< + TParams, + TAlertInstanceContext, + TServices & { ruleDataClient: RuleDataClient } + > +): AlertTypeWithExecutor< + TParams, + TAlertInstanceContext, + TServices & { ruleDataClient: RuleDataClient } +> => { + return { + ...type, + executor: (options) => { + return type.executor({ + ...options, + services: { + ...options.services, + ruleDataClient, + }, + }); + }, + }; +}; diff --git a/x-pack/test/apm_api_integration/configs/index.ts b/x-pack/test/apm_api_integration/configs/index.ts index 97d18c241984..339358015321 100644 --- a/x-pack/test/apm_api_integration/configs/index.ts +++ b/x-pack/test/apm_api_integration/configs/index.ts @@ -18,7 +18,7 @@ const apmFtrConfigs = { rules: { license: 'trial' as const, kibanaConfig: { - 'xpack.ruleRegistry.unsafe.write.enabled': 'true', + 'xpack.ruleRegistry.index': '.kibana-alerts', }, }, }; 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 8d0b87782ff7..e0a3e4d3a3f8 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 @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { get, merge, omit } from 'lodash'; +import { merge, omit } from 'lodash'; import { format } from 'url'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; @@ -30,7 +30,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertestAsApmWriteUser'); const es = getService('es'); - const MAX_POLLS = 5; + const MAX_POLLS = 10; const BULK_INDEX_DELAY = 1000; const INDEXING_DELAY = 5000; @@ -108,11 +108,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { } registry.when('Rule registry with write enabled', { config: 'rules', archives: [] }, () => { - it('bootstraps the apm alert indices', async () => { + it('does not bootstrap indices on plugin startup', async () => { const { body } = await es.indices.get({ index: ALERTS_INDEX_TARGET, expand_wildcards: 'open', - allow_no_indices: false, + allow_no_indices: true, }); const indices = Object.entries(body).map(([indexName, index]) => { @@ -122,23 +122,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; }); - const indexNames = indices.map((index) => index.indexName); - - const apmIndex = indices[0]; - - // make sure it only creates one index - expect(indices.length).to.be(1); - - const apmIndexName = apmIndex.indexName; - - expect(apmIndexName.split('-').includes('observability')).to.be(true); - expect(apmIndexName.split('-').includes('apm')).to.be(true); - - expect(indexNames[0].startsWith('.kibana-alerts-observability-apm')).to.be(true); - - expect(get(apmIndex, 'index.mappings.properties.service.properties.environment.type')).to.be( - 'keyword' - ); + expect(indices.length).to.be(0); }); describe('when creating a rule', () => { @@ -335,12 +319,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { sort: { '@timestamp': 'desc', }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], }, }); expect(afterViolatingDataResponse.body.hits.hits.length).to.be(1); - const alertEvent = afterViolatingDataResponse.body.hits.hits[0]._source as Record< + const alertEvent = afterViolatingDataResponse.body.hits.hits[0].fields as Record< string, any >; @@ -354,23 +340,56 @@ export default function ApiTest({ getService }: FtrProviderContext) { const toCompare = omit(alertEvent, exclude); - expect(toCompare).to.eql({ - 'event.action': 'open', - 'event.kind': 'state', - 'kibana.rac.alert.duration.us': 0, - 'kibana.rac.alert.id': 'apm.transaction_error_rate_opbeans-go_request', - 'kibana.rac.alert.status': 'open', - 'kibana.rac.producer': 'apm', - 'kibana.observability.evaluation.threshold': 30, - 'kibana.observability.evaluation.value': 50, - 'processor.event': 'transaction', - 'rule.category': 'Transaction error rate threshold', - 'rule.id': 'apm.transaction_error_rate', - 'rule.name': 'Transaction error rate threshold | opbeans-go', - 'service.name': 'opbeans-go', - tags: ['apm', 'service.name:opbeans-go'], - 'transaction.type': 'request', - }); + expectSnapshot(toCompare).toMatchInline(` + Object { + "event.action": Array [ + "open", + ], + "event.kind": Array [ + "state", + ], + "kibana.rac.alert.duration.us": Array [ + 0, + ], + "kibana.rac.alert.evaluation.threshold": Array [ + 30, + ], + "kibana.rac.alert.evaluation.value": Array [ + 50, + ], + "kibana.rac.alert.id": Array [ + "apm.transaction_error_rate_opbeans-go_request", + ], + "kibana.rac.alert.producer": Array [ + "apm", + ], + "kibana.rac.alert.status": Array [ + "open", + ], + "processor.event": Array [ + "transaction", + ], + "rule.category": Array [ + "Transaction error rate threshold", + ], + "rule.id": Array [ + "apm.transaction_error_rate", + ], + "rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "service.name": Array [ + "opbeans-go", + ], + "tags": Array [ + "apm", + "service.name:opbeans-go", + ], + "transaction.type": Array [ + "request", + ], + } + `); const now = new Date().getTime(); @@ -390,7 +409,56 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(topAlerts.length).to.be.greaterThan(0); - expect(omit(topAlerts[0], exclude)).to.eql(toCompare); + expectSnapshot(omit(topAlerts[0], exclude)).toMatchInline(` + Object { + "event.action": Array [ + "open", + ], + "event.kind": Array [ + "state", + ], + "kibana.rac.alert.duration.us": Array [ + 0, + ], + "kibana.rac.alert.evaluation.threshold": Array [ + 30, + ], + "kibana.rac.alert.evaluation.value": Array [ + 50, + ], + "kibana.rac.alert.id": Array [ + "apm.transaction_error_rate_opbeans-go_request", + ], + "kibana.rac.alert.producer": Array [ + "apm", + ], + "kibana.rac.alert.status": Array [ + "open", + ], + "processor.event": Array [ + "transaction", + ], + "rule.category": Array [ + "Transaction error rate threshold", + ], + "rule.id": Array [ + "apm.transaction_error_rate", + ], + "rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "service.name": Array [ + "opbeans-go", + ], + "tags": Array [ + "apm", + "service.name:opbeans-go", + ], + "transaction.type": Array [ + "request", + ], + } + `); await es.bulk({ index: APM_TRANSACTION_INDEX_NAME, @@ -423,43 +491,76 @@ export default function ApiTest({ getService }: FtrProviderContext) { sort: { '@timestamp': 'desc', }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], }, }); expect(afterRecoveryResponse.body.hits.hits.length).to.be(1); - const recoveredAlertEvent = afterRecoveryResponse.body.hits.hits[0]._source as Record< + const recoveredAlertEvent = afterRecoveryResponse.body.hits.hits[0].fields as Record< string, any >; - expect(recoveredAlertEvent['kibana.rac.alert.status']).to.eql('closed'); - expect(recoveredAlertEvent['kibana.rac.alert.duration.us']).to.be.greaterThan(0); - expect(new Date(recoveredAlertEvent['kibana.rac.alert.end']).getTime()).to.be.greaterThan( - 0 - ); - + expect(recoveredAlertEvent['kibana.rac.alert.status']?.[0]).to.eql('closed'); + expect(recoveredAlertEvent['kibana.rac.alert.duration.us']?.[0]).to.be.greaterThan(0); expect( + new Date(recoveredAlertEvent['kibana.rac.alert.end']?.[0]).getTime() + ).to.be.greaterThan(0); + + expectSnapshot( omit( recoveredAlertEvent, exclude.concat(['kibana.rac.alert.duration.us', 'kibana.rac.alert.end']) ) - ).to.eql({ - 'event.action': 'close', - 'event.kind': 'state', - 'kibana.rac.alert.id': 'apm.transaction_error_rate_opbeans-go_request', - 'kibana.rac.alert.status': 'closed', - 'kibana.rac.producer': 'apm', - 'kibana.observability.evaluation.threshold': 30, - 'kibana.observability.evaluation.value': 50, - 'processor.event': 'transaction', - 'rule.category': 'Transaction error rate threshold', - 'rule.id': 'apm.transaction_error_rate', - 'rule.name': 'Transaction error rate threshold | opbeans-go', - 'service.name': 'opbeans-go', - tags: ['apm', 'service.name:opbeans-go'], - 'transaction.type': 'request', - }); + ).toMatchInline(` + Object { + "event.action": Array [ + "close", + ], + "event.kind": Array [ + "state", + ], + "kibana.rac.alert.evaluation.threshold": Array [ + 30, + ], + "kibana.rac.alert.evaluation.value": Array [ + 50, + ], + "kibana.rac.alert.id": Array [ + "apm.transaction_error_rate_opbeans-go_request", + ], + "kibana.rac.alert.producer": Array [ + "apm", + ], + "kibana.rac.alert.status": Array [ + "closed", + ], + "processor.event": Array [ + "transaction", + ], + "rule.category": Array [ + "Transaction error rate threshold", + ], + "rule.id": Array [ + "apm.transaction_error_rate", + ], + "rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "service.name": Array [ + "opbeans-go", + ], + "tags": Array [ + "apm", + "service.name:opbeans-go", + ], + "transaction.type": Array [ + "request", + ], + } + `); const { body: topAlertsAfterRecovery, @@ -480,7 +581,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(topAlertsAfterRecovery.length).to.be(1); - expect(topAlertsAfterRecovery[0]['kibana.rac.alert.status']).to.be('closed'); + expect(topAlertsAfterRecovery[0]['kibana.rac.alert.status']?.[0]).to.be('closed'); }); }); }); diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index 44cd2cda7e1a..50194552aec0 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -41,7 +41,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return (await testSubjects.getVisibleText('sectionHeading')) === 'Index Lifecycle Policies'; }); + await pageObjects.indexLifecycleManagement.increasePolicyListPageSize(); + const allPolicies = await pageObjects.indexLifecycleManagement.getPolicyList(); + const filteredPolicies = allPolicies.filter(function (policy) { return policy.name === policyName; }); diff --git a/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts b/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts index 525e0d91e2f4..2dd70f8a9571 100644 --- a/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts +++ b/x-pack/test/functional/page_objects/index_lifecycle_management_page.ts @@ -69,6 +69,11 @@ export function IndexLifecycleManagementPageProvider({ getService }: FtrProvider await this.saveNewPolicy(); }, + async increasePolicyListPageSize() { + await testSubjects.click('tablePaginationPopoverButton'); + await testSubjects.click(`tablePagination-100-rows`); + }, + async getPolicyList() { const policies = await testSubjects.findAll('policyTableRow'); return mapAsync(policies, async (policy) => { diff --git a/yarn.lock b/yarn.lock index e5a0d40728f8..69d5c9553a3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2698,6 +2698,10 @@ version "0.0.0" uid "" +"@kbn/rule-data-utils@link:packages/kbn-rule-data-utils": + version "0.0.0" + uid "" + "@kbn/securitysolution-constants@link:bazel-bin/packages/kbn-securitysolution-constants/npm_module": version "0.0.0" uid ""