diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 0fe1347d299f9..396b62a4e6b80 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -283,6 +283,8 @@ export class DocLinksService { }, transforms: { guide: `${ELASTICSEARCH_DOCS}transforms.html`, + // TODO add valid docs URL + alertingRules: `${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-configuring-alerts.html`, }, visualize: { guide: `${KIBANA_DOCS}dashboard.html`, diff --git a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts index 67687045f1b50..453a29b5884e6 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_usage_collector.ts @@ -17,6 +17,7 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { // Built-in '__index-threshold': { type: 'long' }, '__es-query': { type: 'long' }, + transform_health: { type: 'long' }, // APM apm__error_rate: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention apm__transaction_error_rate: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention @@ -45,8 +46,8 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { // Maps '__geo-containment': { type: 'long' }, // ML - xpack_ml_anomaly_detection_alert: { type: 'long' }, - xpack_ml_anomaly_detection_jobs_health: { type: 'long' }, + xpack__ml__anomaly_detection_alert: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention + xpack__ml__anomaly_detection_jobs_health: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention }; export function createAlertsUsageCollector( diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 18eb8fd4d4ddb..25fffd94d86a4 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -128,6 +128,7 @@ export class MonitoringPlugin for (const alert of alerts) { plugins.alerting?.registerType(alert.getRuleType()); } + const config = createConfig(this.initializerContext.config.get>()); // Register collector objects for stats to show up in the APIs diff --git a/x-pack/plugins/stack_alerts/server/feature.test.ts b/x-pack/plugins/stack_alerts/server/feature.test.ts index 2836dd1d6d8e7..61d0914fb7df1 100644 --- a/x-pack/plugins/stack_alerts/server/feature.test.ts +++ b/x-pack/plugins/stack_alerts/server/feature.test.ts @@ -32,10 +32,15 @@ describe('Stack Alerts Feature Privileges', () => { BUILT_IN_ALERTS_FEATURE.privileges?.all?.alerting?.rule?.all ?? []; const typesInFeaturePrivilegeRead = BUILT_IN_ALERTS_FEATURE.privileges?.read?.alerting?.rule?.read ?? []; - expect(alertingSetup.registerType.mock.calls.length).toEqual(typesInFeaturePrivilege.length); - expect(alertingSetup.registerType.mock.calls.length).toEqual(typesInFeaturePrivilegeAll.length); + // transform alerting rule is initialized during the transform plugin setup expect(alertingSetup.registerType.mock.calls.length).toEqual( - typesInFeaturePrivilegeRead.length + typesInFeaturePrivilege.length - 1 + ); + expect(alertingSetup.registerType.mock.calls.length).toEqual( + typesInFeaturePrivilegeAll.length - 1 + ); + expect(alertingSetup.registerType.mock.calls.length).toEqual( + typesInFeaturePrivilegeRead.length - 1 ); alertingSetup.registerType.mock.calls.forEach((call) => { diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index 70e68c2b7ced3..39ea41374df7b 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -12,6 +12,9 @@ import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containm import { ES_QUERY_ID as ElasticsearchQuery } from './alert_types/es_query/alert_type'; import { STACK_ALERTS_FEATURE_ID } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import { TRANSFORM_RULE_TYPE } from '../../transform/common'; + +const TransformHealth = TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH; export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { id: STACK_ALERTS_FEATURE_ID, @@ -23,7 +26,7 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { management: { insightsAndAlerting: ['triggersActions'], }, - alerting: [IndexThreshold, GeoContainment, ElasticsearchQuery], + alerting: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], privileges: { all: { app: [], @@ -33,10 +36,10 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { }, alerting: { rule: { - all: [IndexThreshold, GeoContainment, ElasticsearchQuery], + all: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], }, alert: { - all: [IndexThreshold, GeoContainment, ElasticsearchQuery], + all: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], }, }, savedObject: { @@ -54,10 +57,10 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { }, alerting: { rule: { - read: [IndexThreshold, GeoContainment, ElasticsearchQuery], + read: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], }, alert: { - read: [IndexThreshold, GeoContainment, ElasticsearchQuery], + read: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth], }, }, savedObject: { diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index f3ae4509f35be..ab3864342e57d 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -19,6 +19,7 @@ { "path": "../triggers_actions_ui/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" } + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../transform/tsconfig.json" } ] } diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 98c81a6f9c677..5bb559c137390 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -163,6 +163,9 @@ "__es-query": { "type": "long" }, + "transform_health": { + "type": "long" + }, "apm__error_rate": { "type": "long" }, @@ -226,10 +229,10 @@ "__geo-containment": { "type": "long" }, - "xpack_ml_anomaly_detection_alert": { + "xpack__ml__anomaly_detection_alert": { "type": "long" }, - "xpack_ml_anomaly_detection_jobs_health": { + "xpack__ml__anomaly_detection_jobs_health": { "type": "long" } } @@ -245,6 +248,9 @@ "__es-query": { "type": "long" }, + "transform_health": { + "type": "long" + }, "apm__error_rate": { "type": "long" }, @@ -308,10 +314,10 @@ "__geo-containment": { "type": "long" }, - "xpack_ml_anomaly_detection_alert": { + "xpack__ml__anomaly_detection_alert": { "type": "long" }, - "xpack_ml_anomaly_detection_jobs_health": { + "xpack__ml__anomaly_detection_jobs_health": { "type": "long" } } diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index 84e43f1f632a1..ee1efe53f0fec 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { LicenseType } from '../../licensing/common/types'; +import { TransformHealthTests } from './types/alerting'; export const DEFAULT_REFRESH_INTERVAL_MS = 30000; export const MINIMUM_REFRESH_INTERVAL_MS = 1000; @@ -108,3 +109,26 @@ export const TRANSFORM_FUNCTION = { } as const; export type TransformFunction = typeof TRANSFORM_FUNCTION[keyof typeof TRANSFORM_FUNCTION]; + +export const TRANSFORM_RULE_TYPE = { + TRANSFORM_HEALTH: 'transform_health', +} as const; + +export const ALL_TRANSFORMS_SELECTION = '*'; + +export const TRANSFORM_HEALTH_CHECK_NAMES: Record< + TransformHealthTests, + { name: string; description: string } +> = { + notStarted: { + name: i18n.translate('xpack.transform.alertTypes.transformHealth.notStartedCheckName', { + defaultMessage: 'Transform is not started', + }), + description: i18n.translate( + 'xpack.transform.alertTypes.transformHealth.notStartedCheckDescription', + { + defaultMessage: 'Get alerts when the transform is not started or is not indexing data.', + } + ), + }, +}; diff --git a/x-pack/plugins/transform/common/index.ts b/x-pack/plugins/transform/common/index.ts new file mode 100644 index 0000000000000..3b2ac4da14c6a --- /dev/null +++ b/x-pack/plugins/transform/common/index.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 { TRANSFORM_RULE_TYPE } from './constants'; diff --git a/x-pack/plugins/transform/common/types/alerting.ts b/x-pack/plugins/transform/common/types/alerting.ts new file mode 100644 index 0000000000000..48a80a3889107 --- /dev/null +++ b/x-pack/plugins/transform/common/types/alerting.ts @@ -0,0 +1,22 @@ +/* + * 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 { AlertTypeParams } from '../../../alerting/common'; + +export type TransformHealthRuleParams = { + includeTransforms?: string[]; + excludeTransforms?: string[] | null; + testsConfig?: { + notStarted?: { + enabled: boolean; + } | null; + } | null; +} & AlertTypeParams; + +export type TransformHealthRuleTestsConfig = TransformHealthRuleParams['testsConfig']; + +export type TransformHealthTests = keyof Exclude; diff --git a/x-pack/plugins/transform/common/types/common.ts b/x-pack/plugins/transform/common/types/common.ts index 1cbb370b0a3ab..94cb935f52634 100644 --- a/x-pack/plugins/transform/common/types/common.ts +++ b/x-pack/plugins/transform/common/types/common.ts @@ -20,3 +20,7 @@ export function dictionaryToArray(dict: Dictionary): TValue[] { export type DeepPartial = { [P in keyof T]?: DeepPartial; }; + +export function isDefined(argument: T | undefined | null): argument is T { + return argument !== undefined && argument !== null; +} diff --git a/x-pack/plugins/transform/common/utils/alerts.ts b/x-pack/plugins/transform/common/utils/alerts.ts new file mode 100644 index 0000000000000..9b3cb2757100a --- /dev/null +++ b/x-pack/plugins/transform/common/utils/alerts.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 type { TransformHealthRuleTestsConfig } from '../types/alerting'; + +export function getResultTestConfig(config: TransformHealthRuleTestsConfig) { + return { + notStarted: { + enabled: config?.notStarted?.enabled ?? true, + }, + }; +} diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index c9f6beeee5aff..5e1c1fb938a86 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -10,11 +10,14 @@ "management", "features", "savedObjects", - "share" + "share", + "triggersActionsUi", + "fieldFormats" ], "optionalPlugins": [ "security", - "usageCollection" + "usageCollection", + "alerting" ], "configPath": ["xpack", "transform"], "requiredBundles": [ @@ -24,6 +27,9 @@ "kibanaReact", "ml" ], + "extraPublicDirs": [ + "common" + ], "owner": { "name": "Machine Learning UI", "githubTeam": "ml-ui" diff --git a/x-pack/plugins/transform/public/alerting/index.ts b/x-pack/plugins/transform/public/alerting/index.ts new file mode 100644 index 0000000000000..0c693cc4bfc06 --- /dev/null +++ b/x-pack/plugins/transform/public/alerting/index.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 { getTransformHealthRuleType } from './transform_health_rule_type'; diff --git a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/index.ts b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/index.ts new file mode 100644 index 0000000000000..87ced49577a91 --- /dev/null +++ b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/index.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 { getTransformHealthRuleType } from './register_transform_health_rule'; diff --git a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/register_transform_health_rule.ts b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/register_transform_health_rule.ts new file mode 100644 index 0000000000000..83117966f73d1 --- /dev/null +++ b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/register_transform_health_rule.ts @@ -0,0 +1,66 @@ +/* + * 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 { lazy } from 'react'; +import { i18n } from '@kbn/i18n'; +import { TRANSFORM_RULE_TYPE } from '../../../common'; +import type { TransformHealthRuleParams } from '../../../common/types/alerting'; +import type { AlertTypeModel } from '../../../../triggers_actions_ui/public'; + +export function getTransformHealthRuleType(): AlertTypeModel { + return { + id: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH, + description: i18n.translate('xpack.transform.alertingRuleTypes.transformHealth.description', { + defaultMessage: 'Alert when transforms experience operational issues.', + }), + iconClass: 'bell', + documentationUrl(docLinks) { + return docLinks.links.transforms.alertingRules; + }, + alertParamsExpression: lazy(() => import('./transform_health_rule_trigger')), + validate: (alertParams: TransformHealthRuleParams) => { + const validationResult = { + errors: { + includeTransforms: new Array(), + } as Record, + }; + + if (!alertParams.includeTransforms?.length) { + validationResult.errors.includeTransforms?.push( + i18n.translate( + 'xpack.transform.alertTypes.transformHealth.includeTransforms.errorMessage', + { + defaultMessage: 'At least one transform has to be selected', + } + ) + ); + } + + return validationResult; + }, + requiresAppContext: false, + defaultActionMessage: i18n.translate( + 'xpack.transform.alertTypes.transformHealth.defaultActionMessage', + { + defaultMessage: `[\\{\\{rule.name\\}\\}] Transform health check result: +\\{\\{context.message\\}\\} +\\{\\{#context.results\\}\\} + Transform ID: \\{\\{transform_id\\}\\} + \\{\\{#description\\}\\}Transform description: \\{\\{description\\}\\} + \\{\\{/description\\}\\}\\{\\{#transform_state\\}\\}Transform state: \\{\\{transform_state\\}\\} + \\{\\{/transform_state\\}\\}\\{\\{#failure_reason\\}\\}Failure reason: \\{\\{failure_reason\\}\\} + \\{\\{/failure_reason\\}\\}\\{\\{#notification_message\\}\\}Notification message: \\{\\{notification_message\\}\\} + \\{\\{/notification_message\\}\\}\\{\\{#node_name\\}\\}Node name: \\{\\{node_name\\}\\} + \\{\\{/node_name\\}\\}\\{\\{#timestamp\\}\\}Timestamp: \\{\\{timestamp\\}\\} + \\{\\{/timestamp\\}\\} + +\\{\\{/context.results\\}\\} +`, + } + ), + }; +} diff --git a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/tests_selection_control.tsx b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/tests_selection_control.tsx new file mode 100644 index 0000000000000..cd00b21862364 --- /dev/null +++ b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/tests_selection_control.tsx @@ -0,0 +1,81 @@ +/* + * 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 React, { FC, useCallback } from 'react'; +import { EuiDescribedFormGroup, EuiForm, EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + TransformHealthRuleTestsConfig, + TransformHealthTests, +} from '../../../common/types/alerting'; +import { getResultTestConfig } from '../../../common/utils/alerts'; +import { TRANSFORM_HEALTH_CHECK_NAMES } from '../../../common/constants'; + +interface TestsSelectionControlProps { + config: TransformHealthRuleTestsConfig; + onChange: (update: TransformHealthRuleTestsConfig) => void; + errors?: string[]; +} + +export const TestsSelectionControl: FC = React.memo( + ({ config, onChange, errors }) => { + const uiConfig = getResultTestConfig(config); + + const updateCallback = useCallback( + (update: Partial>) => { + onChange({ + ...(config ?? {}), + ...update, + }); + }, + [onChange, config] + ); + + return ( + <> + + {( + Object.entries(uiConfig) as Array< + [TransformHealthTests, typeof uiConfig[TransformHealthTests]] + > + ).map(([name, conf], i) => { + return ( + {TRANSFORM_HEALTH_CHECK_NAMES[name]?.name}} + description={TRANSFORM_HEALTH_CHECK_NAMES[name]?.description} + fullWidth + gutterSize={'s'} + > + + + } + onChange={updateCallback.bind(null, { + [name]: { + ...uiConfig[name], + enabled: !uiConfig[name].enabled, + }, + })} + checked={uiConfig[name].enabled} + /> + + + ); + })} + + + + ); + } +); diff --git a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_health_rule_trigger.tsx b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_health_rule_trigger.tsx new file mode 100644 index 0000000000000..c3e4046a30626 --- /dev/null +++ b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_health_rule_trigger.tsx @@ -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 { EuiForm, EuiSpacer } from '@elastic/eui'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import type { AlertTypeParamsExpressionProps } from '../../../../triggers_actions_ui/public'; +import type { TransformHealthRuleParams } from '../../../common/types/alerting'; +import { TestsSelectionControl } from './tests_selection_control'; +import { TransformSelectorControl } from './transform_selector_control'; +import { useApi } from '../../app/hooks'; +import { useToastNotifications } from '../../app/app_dependencies'; +import { GetTransformsResponseSchema } from '../../../common/api_schemas/transforms'; +import { ALL_TRANSFORMS_SELECTION } from '../../../common/constants'; + +export type TransformHealthRuleTriggerProps = + AlertTypeParamsExpressionProps; + +const TransformHealthRuleTrigger: FC = ({ + alertParams, + setAlertParams, + errors, +}) => { + const formErrors = Object.values(errors).flat(); + const isFormInvalid = formErrors.length > 0; + + const api = useApi(); + const toast = useToastNotifications(); + const [transformOptions, setTransformOptions] = useState([]); + + const onAlertParamChange = useCallback( + (param: T) => + (update: TransformHealthRuleParams[T]) => { + setAlertParams(param, update); + }, + [setAlertParams] + ); + + useEffect( + function fetchTransforms() { + let unmounted = false; + api + .getTransforms() + .then((r) => { + if (!unmounted) { + setTransformOptions( + (r as GetTransformsResponseSchema).transforms.filter((v) => v.sync).map((v) => v.id) + ); + } + }) + .catch((e) => { + toast.addError(e, { + title: i18n.translate( + 'xpack.transform.alertingRuleTypes.transformHealth.fetchErrorMessage', + { + defaultMessage: 'Unable to fetch transforms', + } + ), + }); + }); + return () => { + unmounted = true; + }; + }, + [api, toast] + ); + + const excludeTransformOptions = useMemo(() => { + if (alertParams.includeTransforms?.some((v) => v === ALL_TRANSFORMS_SELECTION)) { + return transformOptions; + } + return null; + }, [transformOptions, alertParams.includeTransforms]); + + return ( + + + } + options={transformOptions} + selectedOptions={alertParams.includeTransforms ?? []} + onChange={onAlertParamChange('includeTransforms')} + allowSelectAll + errors={errors.includeTransforms as string[]} + /> + + + + {!!excludeTransformOptions?.length || !!alertParams.excludeTransforms?.length ? ( + <> + + } + options={excludeTransformOptions ?? []} + selectedOptions={alertParams.excludeTransforms ?? []} + onChange={onAlertParamChange('excludeTransforms')} + /> + + + ) : null} + + + + ); +}; + +// Default export is required for React.lazy loading + +// eslint-disable-next-line import/no-default-export +export default TransformHealthRuleTrigger; diff --git a/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_selector_control.tsx b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_selector_control.tsx new file mode 100644 index 0000000000000..4300b75cb3fa4 --- /dev/null +++ b/x-pack/plugins/transform/public/alerting/transform_health_rule_type/transform_selector_control.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiComboBox, EuiComboBoxProps, EuiFormRow } from '@elastic/eui'; +import React, { FC, useMemo } from 'react'; +import { ALL_TRANSFORMS_SELECTION } from '../../../common/constants'; +import { isDefined } from '../../../common/types/common'; + +export interface TransformSelectorControlProps { + label?: string | JSX.Element; + errors?: string[]; + onChange: (transformSelection: string[]) => void; + selectedOptions: string[]; + options: string[]; + allowSelectAll?: boolean; +} + +function convertToEuiOptions(values: string[]) { + return values.map((v) => ({ value: v, label: v })); +} + +export const TransformSelectorControl: FC = ({ + label, + errors, + onChange, + selectedOptions, + options, + allowSelectAll = false, +}) => { + const onSelectionChange: EuiComboBoxProps['onChange'] = ((selectionUpdate) => { + if (!selectionUpdate?.length) { + onChange([]); + return; + } + if (selectionUpdate[selectionUpdate.length - 1].value === ALL_TRANSFORMS_SELECTION) { + onChange([ALL_TRANSFORMS_SELECTION]); + return; + } + onChange( + selectionUpdate + .slice(selectionUpdate[0].value === ALL_TRANSFORMS_SELECTION ? 1 : 0) + .map((v) => v.value) + .filter(isDefined) + ); + }) as Exclude['onChange'], undefined>; + + const selectedOptionsEui = useMemo(() => convertToEuiOptions(selectedOptions), [selectedOptions]); + const optionsEui = useMemo(() => { + return convertToEuiOptions(allowSelectAll ? [ALL_TRANSFORMS_SELECTION, ...options] : options); + }, [options, allowSelectAll]); + + return ( + + + singleSelection={false} + selectedOptions={selectedOptionsEui} + options={optionsEui} + onChange={onSelectionChange} + fullWidth + data-test-subj={'transformSelection'} + isInvalid={!!errors?.length} + /> + + ); +}; diff --git a/x-pack/plugins/transform/public/index.ts b/x-pack/plugins/transform/public/index.ts index e4f630e23afce..ebe43aea75440 100644 --- a/x-pack/plugins/transform/public/index.ts +++ b/x-pack/plugins/transform/public/index.ts @@ -12,3 +12,5 @@ import { TransformUiPlugin } from './plugin'; export const plugin = () => { return new TransformUiPlugin(); }; + +export { getTransformHealthRuleType } from './alerting'; diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts index b058be46d677b..4ed4e64070344 100644 --- a/x-pack/plugins/transform/public/plugin.ts +++ b/x-pack/plugins/transform/public/plugin.ts @@ -14,6 +14,9 @@ import type { SavedObjectsStart } from 'src/plugins/saved_objects/public'; import type { ManagementSetup } from 'src/plugins/management/public'; import type { SharePluginStart } from 'src/plugins/share/public'; import { registerFeature } from './register_feature'; +import type { PluginSetupContract as AlertingSetup } from '../../alerting/public'; +import type { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; +import { getTransformHealthRuleType } from './alerting'; export interface PluginsDependencies { data: DataPublicPluginStart; @@ -21,11 +24,13 @@ export interface PluginsDependencies { home: HomePublicPluginSetup; savedObjects: SavedObjectsStart; share: SharePluginStart; + alerting?: AlertingSetup; + triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup; } export class TransformUiPlugin { public setup(coreSetup: CoreSetup, pluginsSetup: PluginsDependencies): void { - const { management, home } = pluginsSetup; + const { management, home, triggersActionsUi } = pluginsSetup; // Register management section const esSection = management.sections.section.data; @@ -41,6 +46,10 @@ export class TransformUiPlugin { }, }); registerFeature(home); + + if (triggersActionsUi) { + triggersActionsUi.ruleTypeRegistry.register(getTransformHealthRuleType()); + } } public start() {} diff --git a/x-pack/plugins/transform/server/index.ts b/x-pack/plugins/transform/server/index.ts index 77103aa4fdac5..9bd3ffe418b1e 100644 --- a/x-pack/plugins/transform/server/index.ts +++ b/x-pack/plugins/transform/server/index.ts @@ -10,3 +10,5 @@ import { PluginInitializerContext } from 'src/core/server'; import { TransformServerPlugin } from './plugin'; export const plugin = (ctx: PluginInitializerContext) => new TransformServerPlugin(ctx); + +export { registerTransformHealthRuleType } from './lib/alerting'; diff --git a/x-pack/plugins/transform/server/lib/alerting/index.ts b/x-pack/plugins/transform/server/lib/alerting/index.ts new file mode 100644 index 0000000000000..1d75af94f2a72 --- /dev/null +++ b/x-pack/plugins/transform/server/lib/alerting/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { + getTransformHealthRuleType, + registerTransformHealthRuleType, +} from './transform_health_rule_type'; diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/index.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/index.ts new file mode 100644 index 0000000000000..cef3d578df658 --- /dev/null +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { + getTransformHealthRuleType, + registerTransformHealthRuleType, +} from './register_transform_health_rule_type'; diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts new file mode 100644 index 0000000000000..eb0cf011aeb52 --- /dev/null +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { Logger } from 'src/core/server'; +import type { + ActionGroup, + AlertInstanceContext, + AlertInstanceState, + AlertTypeState, +} from '../../../../../alerting/common'; +import { PLUGIN, TRANSFORM_RULE_TYPE } from '../../../../common/constants'; +import { transformHealthRuleParams, TransformHealthRuleParams } from './schema'; +import { AlertType } from '../../../../../alerting/server'; +import { transformHealthServiceProvider } from './transform_health_service'; +import type { PluginSetupContract as AlertingSetup } from '../../../../../alerting/server'; + +export interface BaseResponse { + transform_id: string; + description?: string; +} + +export interface NotStartedTransformResponse extends BaseResponse { + transform_state: string; + node_name?: string; +} + +export type TransformHealthResult = NotStartedTransformResponse; + +export type TransformHealthAlertContext = { + results: TransformHealthResult[]; + message: string; +} & AlertInstanceContext; + +export const TRANSFORM_ISSUE = 'transform_issue'; + +export type TransformIssue = typeof TRANSFORM_ISSUE; + +export const TRANSFORM_ISSUE_DETECTED: ActionGroup = { + id: TRANSFORM_ISSUE, + name: i18n.translate('xpack.transform.alertingRuleTypes.transformHealth.actionGroupName', { + defaultMessage: 'Issue detected', + }), +}; + +interface RegisterParams { + logger: Logger; + alerting: AlertingSetup; +} + +export function registerTransformHealthRuleType(params: RegisterParams) { + const { alerting } = params; + alerting.registerType(getTransformHealthRuleType()); +} + +export function getTransformHealthRuleType(): AlertType< + TransformHealthRuleParams, + never, + AlertTypeState, + AlertInstanceState, + TransformHealthAlertContext, + TransformIssue +> { + return { + id: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH, + name: i18n.translate('xpack.transform.alertingRuleTypes.transformHealth.name', { + defaultMessage: 'Transform health', + }), + actionGroups: [TRANSFORM_ISSUE_DETECTED], + defaultActionGroupId: TRANSFORM_ISSUE, + validate: { params: transformHealthRuleParams }, + actionVariables: { + context: [ + { + name: 'results', + description: i18n.translate( + 'xpack.transform.alertTypes.transformHealth.alertContext.resultsDescription', + { + defaultMessage: 'Rule execution results', + } + ), + }, + { + name: 'message', + description: i18n.translate( + 'xpack.transform.alertTypes.transformHealth.alertContext.messageDescription', + { + defaultMessage: 'Alert info message', + } + ), + }, + ], + }, + producer: 'stackAlerts', + minimumLicenseRequired: PLUGIN.MINIMUM_LICENSE_REQUIRED, + isExportable: true, + async executor(options) { + const { + services: { scopedClusterClient, alertInstanceFactory }, + params, + } = options; + + const transformHealthService = transformHealthServiceProvider( + scopedClusterClient.asInternalUser + ); + + const executionResult = await transformHealthService.getHealthChecksResults(params); + + if (executionResult.length > 0) { + executionResult.forEach(({ name: alertInstanceName, context }) => { + const alertInstance = alertInstanceFactory(alertInstanceName); + alertInstance.scheduleActions(TRANSFORM_ISSUE, context); + }); + } + }, + }; +} diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/schema.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/schema.ts new file mode 100644 index 0000000000000..5a7af83b120d6 --- /dev/null +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/schema.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 { schema, TypeOf } from '@kbn/config-schema'; + +export const transformHealthRuleParams = schema.object({ + includeTransforms: schema.arrayOf(schema.string()), + excludeTransforms: schema.nullable(schema.arrayOf(schema.string(), { defaultValue: [] })), + testsConfig: schema.nullable( + schema.object({ + notStarted: schema.nullable( + schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }) + ), + }) + ), +}); + +export type TransformHealthRuleParams = TypeOf; diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts new file mode 100644 index 0000000000000..88b5396c7b110 --- /dev/null +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts @@ -0,0 +1,135 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import type { Transform as EsTransform } from '@elastic/elasticsearch/api/types'; +import { TransformHealthRuleParams } from './schema'; +import { + ALL_TRANSFORMS_SELECTION, + TRANSFORM_HEALTH_CHECK_NAMES, +} from '../../../../common/constants'; +import { getResultTestConfig } from '../../../../common/utils/alerts'; +import { + NotStartedTransformResponse, + TransformHealthAlertContext, +} from './register_transform_health_rule_type'; + +interface TestResult { + name: string; + context: TransformHealthAlertContext; +} + +// @ts-ignore FIXME update types in the elasticsearch client +type Transform = EsTransform & { id: string; description?: string; sync: object }; + +export function transformHealthServiceProvider(esClient: ElasticsearchClient) { + const transformsDict = new Map(); + + /** + * Resolves result transform selection. + * @param includeTransforms + * @param excludeTransforms + */ + const getResultsTransformIds = async ( + includeTransforms: string[], + excludeTransforms: string[] | null + ): Promise => { + const includeAll = includeTransforms.some((id) => id === ALL_TRANSFORMS_SELECTION); + + // Fetch transforms to make sure assigned transforms exists. + const transformsResponse = ( + await esClient.transform.getTransform({ + ...(includeAll ? {} : { transform_id: includeTransforms.join(',') }), + allow_no_match: true, + size: 1000, + }) + ).body.transforms as Transform[]; + + let resultTransformIds: string[] = []; + + transformsResponse.forEach((t) => { + transformsDict.set(t.id, t); + if (t.sync) { + resultTransformIds.push(t.id); + } + }); + + if (excludeTransforms && excludeTransforms.length > 0) { + const excludeIdsSet = new Set(excludeTransforms); + resultTransformIds = resultTransformIds.filter((id) => !excludeIdsSet.has(id)); + } + + return resultTransformIds; + }; + + return { + /** + * Returns report about not started transform + * @param transformIds + */ + async getNotStartedTransformsReport( + transformIds: string[] + ): Promise { + const transformsStats = ( + await esClient.transform.getTransformStats({ + transform_id: transformIds.join(','), + }) + ).body.transforms; + + return transformsStats + .filter((t) => t.state !== 'started' && t.state !== 'indexing') + .map((t) => ({ + transform_id: t.id, + description: transformsDict.get(t.id)?.description, + transform_state: t.state, + node_name: t.node?.name, + })); + }, + /** + * Returns results of the transform health checks + * @param params + */ + async getHealthChecksResults(params: TransformHealthRuleParams) { + const transformIds = await getResultsTransformIds( + params.includeTransforms, + params.excludeTransforms + ); + + const testsConfig = getResultTestConfig(params.testsConfig); + + const result: TestResult[] = []; + + if (testsConfig.notStarted.enabled) { + const response = await this.getNotStartedTransformsReport(transformIds); + if (response.length > 0) { + const count = response.length; + const transformsString = response.map((t) => t.transform_id).join(', '); + + result.push({ + name: TRANSFORM_HEALTH_CHECK_NAMES.notStarted.name, + context: { + results: response, + message: i18n.translate( + 'xpack.transform.alertTypes.transformHealth.notStartedMessage', + { + defaultMessage: + '{count, plural, one {Transform} other {Transforms}} {transformsString} {count, plural, one {is} other {are}} not started.', + values: { count, transformsString }, + } + ), + }, + }); + } + } + + return result; + }, + }; +} + +export type TransformHealthService = ReturnType; diff --git a/x-pack/plugins/transform/server/plugin.ts b/x-pack/plugins/transform/server/plugin.ts index c21e131e056b8..6e542bbefc3e1 100644 --- a/x-pack/plugins/transform/server/plugin.ts +++ b/x-pack/plugins/transform/server/plugin.ts @@ -13,6 +13,7 @@ import { LicenseType } from '../../licensing/common/types'; import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { License } from './services'; +import { registerTransformHealthRuleType } from './lib/alerting'; const basicLicense: LicenseType = 'basic'; @@ -38,7 +39,7 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { setup( { http, getStartServices, elasticsearch }: CoreSetup, - { licensing, features }: Dependencies + { licensing, features, alerting }: Dependencies ): {} { const router = http.createRouter(); @@ -75,6 +76,10 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { license: this.license, }); + if (alerting) { + registerTransformHealthRuleType({ alerting, logger: this.logger }); + } + return {}; } diff --git a/x-pack/plugins/transform/server/types.ts b/x-pack/plugins/transform/server/types.ts index a6e1996a45013..53f13cc752650 100644 --- a/x-pack/plugins/transform/server/types.ts +++ b/x-pack/plugins/transform/server/types.ts @@ -9,10 +9,12 @@ import { IRouter } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; +import type { AlertingPlugin } from '../../alerting/server'; export interface Dependencies { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; + alerting?: AlertingPlugin['setup']; } export interface RouteDependencies { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 3a4cc62c2550f..531046013263f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -35,6 +35,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + loadTestFile(require.resolve('./transform_rule_types')); loadTestFile(require.resolve('./mustache_templates.ts')); loadTestFile(require.resolve('./notify_when')); loadTestFile(require.resolve('./ephemeral')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/index.ts new file mode 100644 index 0000000000000..072e318da2df9 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/index.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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: FtrProviderContext) { + describe('transform alert rule types', function () { + this.tags('dima'); + loadTestFile(require.resolve('./transform_health')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/alert.ts new file mode 100644 index 0000000000000..c5fb4ec61aa4f --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/alert.ts @@ -0,0 +1,206 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { + ES_TEST_INDEX_NAME, + ESTestIndexTool, + getUrlPrefix, + ObjectRemover, +} from '../../../../../common/lib'; +import { Spaces } from '../../../../scenarios'; +import { PutTransformsRequestSchema } from '../../../../../../../plugins/transform/common/api_schemas/transforms'; + +const ACTION_TYPE_ID = '.index'; +const ALERT_TYPE_ID = 'transform_health'; +const ES_TEST_INDEX_SOURCE = 'transform-alert:transform-health'; +const ES_TEST_INDEX_REFERENCE = '-na-'; +const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-ts-output`; + +const ALERT_INTERVAL_SECONDS = 3; + +interface CreateAlertParams { + name: string; + includeTransforms: string[]; + excludeTransforms?: string[] | null; + testsConfig?: { + notStarted?: { + enabled: boolean; + } | null; + } | null; +} + +export function generateDestIndex(transformId: string): string { + return `user-${transformId}`; +} + +export function generateTransformConfig(transformId: string): PutTransformsRequestSchema { + const destinationIndex = generateDestIndex(transformId); + + return { + source: { index: ['ft_farequote'] }, + pivot: { + group_by: { airline: { terms: { field: 'airline' } } }, + aggregations: { '@timestamp.value_count': { value_count: { field: '@timestamp' } } }, + }, + dest: { index: destinationIndex }, + sync: { + time: { field: '@timestamp' }, + }, + }; +} + +// eslint-disable-next-line import/no-default-export +export default function alertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + const es = getService('es'); + const log = getService('log'); + const transform = getService('transform'); + + const esTestIndexTool = new ESTestIndexTool(es, retry); + const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); + + describe('alert', async () => { + const objectRemover = new ObjectRemover(supertest); + let actionId: string; + const transformId = 'test_transform_01'; + const destinationIndex = generateDestIndex(transformId); + + beforeEach(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + await esTestIndexToolOutput.destroy(); + await esTestIndexToolOutput.setup(); + + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + + actionId = await createAction(); + + await transform.api.createIndices(destinationIndex); + await createTransform(transformId); + }); + + afterEach(async () => { + await objectRemover.removeAll(); + await esTestIndexTool.destroy(); + await esTestIndexToolOutput.destroy(); + await transform.api.cleanTransformIndices(); + }); + + it('runs correctly', async () => { + await createAlert({ + name: 'Test all transforms', + includeTransforms: ['*'], + }); + + await stopTransform(transformId); + + log.debug('Checking created alert instances...'); + + const docs = await waitForDocs(1); + for (const doc of docs) { + const { name, message } = doc._source.params; + + expect(name).to.be('Test all transforms'); + expect(message).to.be('Transform test_transform_01 is not started.'); + } + }); + + async function waitForDocs(count: number): Promise { + return await esTestIndexToolOutput.waitForDocs( + ES_TEST_INDEX_SOURCE, + ES_TEST_INDEX_REFERENCE, + count + ); + } + + async function createTransform(id: string) { + const config = generateTransformConfig(id); + await transform.api.createAndRunTransform(id, config); + } + + async function createAlert(params: CreateAlertParams): Promise { + log.debug(`Creating an alerting rule "${params.name}"...`); + const action = { + id: actionId, + group: 'transform_issue', + params: { + documents: [ + { + source: ES_TEST_INDEX_SOURCE, + reference: ES_TEST_INDEX_REFERENCE, + params: { + name: '{{{alertName}}}', + message: '{{{context.message}}}', + }, + }, + ], + }, + }; + + const { status, body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send({ + name: params.name, + consumer: 'alerts', + enabled: true, + rule_type_id: ALERT_TYPE_ID, + schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` }, + actions: [action], + notify_when: 'onActiveAlert', + params: { + includeTransforms: params.includeTransforms, + }, + }); + + // will print the error body, if an error occurred + // if (statusCode !== 200) console.log(createdAlert); + + expect(status).to.be(200); + + const alertId = createdAlert.id; + objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); + + return alertId; + } + + async function stopTransform(id: string) { + await transform.api.stopTransform(id); + } + + async function createAction(): Promise { + log.debug('Creating an action...'); + // @ts-ignore + const { statusCode, body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'index action for transform health FT', + connector_type_id: ACTION_TYPE_ID, + config: { + index: ES_TEST_OUTPUT_INDEX_NAME, + }, + secrets: {}, + }); + + expect(statusCode).to.be(200); + + log.debug(`Action with id "${createdAction.id}" has been created.`); + + const resultId = createdAction.id; + objectRemover.add(Spaces.space1.id, resultId, 'connector', 'actions'); + + return resultId; + } + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/index.ts new file mode 100644 index 0000000000000..c324745b85813 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/transform_health/index.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. + */ + +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: FtrProviderContext) { + describe('transform_health', function () { + loadTestFile(require.resolve('./alert')); + }); +} diff --git a/x-pack/test/functional/services/transform/api.ts b/x-pack/test/functional/services/transform/api.ts index 484d794aac879..73dff415832f6 100644 --- a/x-pack/test/functional/services/transform/api.ts +++ b/x-pack/test/functional/services/transform/api.ts @@ -234,6 +234,11 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { await esSupertest.post(`/_transform/${transformId}/_start`).expect(200); }, + async stopTransform(transformId: string) { + log.debug(`Stopping transform '${transformId}' ...`); + await esSupertest.post(`/_transform/${transformId}/_stop`).expect(200); + }, + async createAndRunTransform(transformId: string, transformConfig: PutTransformsRequestSchema) { await this.createTransform(transformId, transformConfig); await this.startTransform(transformId);