mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Transform] Transforms health alerting rule type (#112277)
This commit is contained in:
parent
f8611470e6
commit
6da1323ff5
34 changed files with 1047 additions and 19 deletions
|
@ -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`,
|
||||
|
|
|
@ -17,6 +17,7 @@ const byTypeSchema: MakeSchemaFrom<AlertsUsage>['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<AlertsUsage>['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(
|
||||
|
|
|
@ -128,6 +128,7 @@ export class MonitoringPlugin
|
|||
for (const alert of alerts) {
|
||||
plugins.alerting?.registerType(alert.getRuleType());
|
||||
}
|
||||
|
||||
const config = createConfig(this.initializerContext.config.get<TypeOf<typeof configSchema>>());
|
||||
|
||||
// Register collector objects for stats to show up in the APIs
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.',
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
|
8
x-pack/plugins/transform/common/index.ts
Normal file
8
x-pack/plugins/transform/common/index.ts
Normal file
|
@ -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';
|
22
x-pack/plugins/transform/common/types/alerting.ts
Normal file
22
x-pack/plugins/transform/common/types/alerting.ts
Normal file
|
@ -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<TransformHealthRuleTestsConfig, null | undefined>;
|
|
@ -20,3 +20,7 @@ export function dictionaryToArray<TValue>(dict: Dictionary<TValue>): TValue[] {
|
|||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
|
||||
export function isDefined<T>(argument: T | undefined | null): argument is T {
|
||||
return argument !== undefined && argument !== null;
|
||||
}
|
||||
|
|
16
x-pack/plugins/transform/common/utils/alerts.ts
Normal file
16
x-pack/plugins/transform/common/utils/alerts.ts
Normal file
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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"
|
||||
|
|
8
x-pack/plugins/transform/public/alerting/index.ts
Normal file
8
x-pack/plugins/transform/public/alerting/index.ts
Normal file
|
@ -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';
|
|
@ -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';
|
|
@ -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<TransformHealthRuleParams> {
|
||||
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<string>(),
|
||||
} as Record<keyof TransformHealthRuleParams, string[]>,
|
||||
};
|
||||
|
||||
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\\}\\}
|
||||
`,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
|
@ -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<TestsSelectionControlProps> = React.memo(
|
||||
({ config, onChange, errors }) => {
|
||||
const uiConfig = getResultTestConfig(config);
|
||||
|
||||
const updateCallback = useCallback(
|
||||
(update: Partial<Exclude<TransformHealthRuleTestsConfig, undefined>>) => {
|
||||
onChange({
|
||||
...(config ?? {}),
|
||||
...update,
|
||||
});
|
||||
},
|
||||
[onChange, config]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiForm component="div" isInvalid={!!errors?.length} error={errors}>
|
||||
{(
|
||||
Object.entries(uiConfig) as Array<
|
||||
[TransformHealthTests, typeof uiConfig[TransformHealthTests]]
|
||||
>
|
||||
).map(([name, conf], i) => {
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
key={name}
|
||||
title={<h4>{TRANSFORM_HEALTH_CHECK_NAMES[name]?.name}</h4>}
|
||||
description={TRANSFORM_HEALTH_CHECK_NAMES[name]?.description}
|
||||
fullWidth
|
||||
gutterSize={'s'}
|
||||
>
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
disabled
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.alertTypes.transformHealth.testsSelection.enableTestLabel"
|
||||
defaultMessage="Enable"
|
||||
/>
|
||||
}
|
||||
onChange={updateCallback.bind(null, {
|
||||
[name]: {
|
||||
...uiConfig[name],
|
||||
enabled: !uiConfig[name].enabled,
|
||||
},
|
||||
})}
|
||||
checked={uiConfig[name].enabled}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
})}
|
||||
</EuiForm>
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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<TransformHealthRuleParams>;
|
||||
|
||||
const TransformHealthRuleTrigger: FC<TransformHealthRuleTriggerProps> = ({
|
||||
alertParams,
|
||||
setAlertParams,
|
||||
errors,
|
||||
}) => {
|
||||
const formErrors = Object.values(errors).flat();
|
||||
const isFormInvalid = formErrors.length > 0;
|
||||
|
||||
const api = useApi();
|
||||
const toast = useToastNotifications();
|
||||
const [transformOptions, setTransformOptions] = useState<string[]>([]);
|
||||
|
||||
const onAlertParamChange = useCallback(
|
||||
<T extends keyof TransformHealthRuleParams>(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 (
|
||||
<EuiForm
|
||||
data-test-subj={'transformHealthAlertingRuleForm'}
|
||||
invalidCallout={'none'}
|
||||
error={formErrors}
|
||||
isInvalid={isFormInvalid}
|
||||
>
|
||||
<TransformSelectorControl
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.alertTypes.transformHealth.includeTransformsLabel"
|
||||
defaultMessage="Include transforms"
|
||||
/>
|
||||
}
|
||||
options={transformOptions}
|
||||
selectedOptions={alertParams.includeTransforms ?? []}
|
||||
onChange={onAlertParamChange('includeTransforms')}
|
||||
allowSelectAll
|
||||
errors={errors.includeTransforms as string[]}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{!!excludeTransformOptions?.length || !!alertParams.excludeTransforms?.length ? (
|
||||
<>
|
||||
<TransformSelectorControl
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.alertTypes.transformHealth.excludeTransformsLabel"
|
||||
defaultMessage="Exclude transforms"
|
||||
/>
|
||||
}
|
||||
options={excludeTransformOptions ?? []}
|
||||
selectedOptions={alertParams.excludeTransforms ?? []}
|
||||
onChange={onAlertParamChange('excludeTransforms')}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<TestsSelectionControl
|
||||
config={alertParams.testsConfig}
|
||||
onChange={onAlertParamChange('testsConfig')}
|
||||
errors={Array.isArray(errors.testsConfig) ? errors.testsConfig : []}
|
||||
/>
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
||||
// Default export is required for React.lazy loading
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default TransformHealthRuleTrigger;
|
|
@ -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<TransformSelectorControlProps> = ({
|
||||
label,
|
||||
errors,
|
||||
onChange,
|
||||
selectedOptions,
|
||||
options,
|
||||
allowSelectAll = false,
|
||||
}) => {
|
||||
const onSelectionChange: EuiComboBoxProps<string>['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<EuiComboBoxProps<string>['onChange'], undefined>;
|
||||
|
||||
const selectedOptionsEui = useMemo(() => convertToEuiOptions(selectedOptions), [selectedOptions]);
|
||||
const optionsEui = useMemo(() => {
|
||||
return convertToEuiOptions(allowSelectAll ? [ALL_TRANSFORMS_SELECTION, ...options] : options);
|
||||
}, [options, allowSelectAll]);
|
||||
|
||||
return (
|
||||
<EuiFormRow fullWidth label={label} isInvalid={!!errors?.length} error={errors}>
|
||||
<EuiComboBox<string>
|
||||
singleSelection={false}
|
||||
selectedOptions={selectedOptionsEui}
|
||||
options={optionsEui}
|
||||
onChange={onSelectionChange}
|
||||
fullWidth
|
||||
data-test-subj={'transformSelection'}
|
||||
isInvalid={!!errors?.length}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -12,3 +12,5 @@ import { TransformUiPlugin } from './plugin';
|
|||
export const plugin = () => {
|
||||
return new TransformUiPlugin();
|
||||
};
|
||||
|
||||
export { getTransformHealthRuleType } from './alerting';
|
||||
|
|
|
@ -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<PluginsDependencies>, 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() {}
|
||||
|
|
|
@ -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';
|
||||
|
|
11
x-pack/plugins/transform/server/lib/alerting/index.ts
Normal file
11
x-pack/plugins/transform/server/lib/alerting/index.ts
Normal file
|
@ -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';
|
|
@ -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';
|
|
@ -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<TransformIssue> = {
|
||||
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);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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<typeof transformHealthRuleParams>;
|
|
@ -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<string, Transform>();
|
||||
|
||||
/**
|
||||
* Resolves result transform selection.
|
||||
* @param includeTransforms
|
||||
* @param excludeTransforms
|
||||
*/
|
||||
const getResultsTransformIds = async (
|
||||
includeTransforms: string[],
|
||||
excludeTransforms: string[] | null
|
||||
): Promise<string[]> => {
|
||||
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<NotStartedTransformResponse[]> {
|
||||
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<typeof transformHealthServiceProvider>;
|
|
@ -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 {};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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<any[]> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue