mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -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: {
|
transforms: {
|
||||||
guide: `${ELASTICSEARCH_DOCS}transforms.html`,
|
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: {
|
visualize: {
|
||||||
guide: `${KIBANA_DOCS}dashboard.html`,
|
guide: `${KIBANA_DOCS}dashboard.html`,
|
||||||
|
|
|
@ -17,6 +17,7 @@ const byTypeSchema: MakeSchemaFrom<AlertsUsage>['count_by_type'] = {
|
||||||
// Built-in
|
// Built-in
|
||||||
'__index-threshold': { type: 'long' },
|
'__index-threshold': { type: 'long' },
|
||||||
'__es-query': { type: 'long' },
|
'__es-query': { type: 'long' },
|
||||||
|
transform_health: { type: 'long' },
|
||||||
// APM
|
// APM
|
||||||
apm__error_rate: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention
|
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
|
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
|
// Maps
|
||||||
'__geo-containment': { type: 'long' },
|
'__geo-containment': { type: 'long' },
|
||||||
// ML
|
// ML
|
||||||
xpack_ml_anomaly_detection_alert: { type: 'long' },
|
xpack__ml__anomaly_detection_alert: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
xpack_ml_anomaly_detection_jobs_health: { type: 'long' },
|
xpack__ml__anomaly_detection_jobs_health: { type: 'long' }, // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createAlertsUsageCollector(
|
export function createAlertsUsageCollector(
|
||||||
|
|
|
@ -128,6 +128,7 @@ export class MonitoringPlugin
|
||||||
for (const alert of alerts) {
|
for (const alert of alerts) {
|
||||||
plugins.alerting?.registerType(alert.getRuleType());
|
plugins.alerting?.registerType(alert.getRuleType());
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = createConfig(this.initializerContext.config.get<TypeOf<typeof configSchema>>());
|
const config = createConfig(this.initializerContext.config.get<TypeOf<typeof configSchema>>());
|
||||||
|
|
||||||
// Register collector objects for stats to show up in the APIs
|
// 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 ?? [];
|
BUILT_IN_ALERTS_FEATURE.privileges?.all?.alerting?.rule?.all ?? [];
|
||||||
const typesInFeaturePrivilegeRead =
|
const typesInFeaturePrivilegeRead =
|
||||||
BUILT_IN_ALERTS_FEATURE.privileges?.read?.alerting?.rule?.read ?? [];
|
BUILT_IN_ALERTS_FEATURE.privileges?.read?.alerting?.rule?.read ?? [];
|
||||||
expect(alertingSetup.registerType.mock.calls.length).toEqual(typesInFeaturePrivilege.length);
|
// transform alerting rule is initialized during the transform plugin setup
|
||||||
expect(alertingSetup.registerType.mock.calls.length).toEqual(typesInFeaturePrivilegeAll.length);
|
|
||||||
expect(alertingSetup.registerType.mock.calls.length).toEqual(
|
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) => {
|
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 { ES_QUERY_ID as ElasticsearchQuery } from './alert_types/es_query/alert_type';
|
||||||
import { STACK_ALERTS_FEATURE_ID } from '../common';
|
import { STACK_ALERTS_FEATURE_ID } from '../common';
|
||||||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
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 = {
|
export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = {
|
||||||
id: STACK_ALERTS_FEATURE_ID,
|
id: STACK_ALERTS_FEATURE_ID,
|
||||||
|
@ -23,7 +26,7 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = {
|
||||||
management: {
|
management: {
|
||||||
insightsAndAlerting: ['triggersActions'],
|
insightsAndAlerting: ['triggersActions'],
|
||||||
},
|
},
|
||||||
alerting: [IndexThreshold, GeoContainment, ElasticsearchQuery],
|
alerting: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth],
|
||||||
privileges: {
|
privileges: {
|
||||||
all: {
|
all: {
|
||||||
app: [],
|
app: [],
|
||||||
|
@ -33,10 +36,10 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = {
|
||||||
},
|
},
|
||||||
alerting: {
|
alerting: {
|
||||||
rule: {
|
rule: {
|
||||||
all: [IndexThreshold, GeoContainment, ElasticsearchQuery],
|
all: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth],
|
||||||
},
|
},
|
||||||
alert: {
|
alert: {
|
||||||
all: [IndexThreshold, GeoContainment, ElasticsearchQuery],
|
all: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
savedObject: {
|
savedObject: {
|
||||||
|
@ -54,10 +57,10 @@ export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = {
|
||||||
},
|
},
|
||||||
alerting: {
|
alerting: {
|
||||||
rule: {
|
rule: {
|
||||||
read: [IndexThreshold, GeoContainment, ElasticsearchQuery],
|
read: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth],
|
||||||
},
|
},
|
||||||
alert: {
|
alert: {
|
||||||
read: [IndexThreshold, GeoContainment, ElasticsearchQuery],
|
read: [IndexThreshold, GeoContainment, ElasticsearchQuery, TransformHealth],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
savedObject: {
|
savedObject: {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
{ "path": "../triggers_actions_ui/tsconfig.json" },
|
{ "path": "../triggers_actions_ui/tsconfig.json" },
|
||||||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||||
{ "path": "../../../src/plugins/saved_objects/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": {
|
"__es-query": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
|
"transform_health": {
|
||||||
|
"type": "long"
|
||||||
|
},
|
||||||
"apm__error_rate": {
|
"apm__error_rate": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
|
@ -226,10 +229,10 @@
|
||||||
"__geo-containment": {
|
"__geo-containment": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
"xpack_ml_anomaly_detection_alert": {
|
"xpack__ml__anomaly_detection_alert": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
"xpack_ml_anomaly_detection_jobs_health": {
|
"xpack__ml__anomaly_detection_jobs_health": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,6 +248,9 @@
|
||||||
"__es-query": {
|
"__es-query": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
|
"transform_health": {
|
||||||
|
"type": "long"
|
||||||
|
},
|
||||||
"apm__error_rate": {
|
"apm__error_rate": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
|
@ -308,10 +314,10 @@
|
||||||
"__geo-containment": {
|
"__geo-containment": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
"xpack_ml_anomaly_detection_alert": {
|
"xpack__ml__anomaly_detection_alert": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
},
|
},
|
||||||
"xpack_ml_anomaly_detection_jobs_health": {
|
"xpack__ml__anomaly_detection_jobs_health": {
|
||||||
"type": "long"
|
"type": "long"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
import { LicenseType } from '../../licensing/common/types';
|
import { LicenseType } from '../../licensing/common/types';
|
||||||
|
import { TransformHealthTests } from './types/alerting';
|
||||||
|
|
||||||
export const DEFAULT_REFRESH_INTERVAL_MS = 30000;
|
export const DEFAULT_REFRESH_INTERVAL_MS = 30000;
|
||||||
export const MINIMUM_REFRESH_INTERVAL_MS = 1000;
|
export const MINIMUM_REFRESH_INTERVAL_MS = 1000;
|
||||||
|
@ -108,3 +109,26 @@ export const TRANSFORM_FUNCTION = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type TransformFunction = typeof TRANSFORM_FUNCTION[keyof typeof TRANSFORM_FUNCTION];
|
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> = {
|
export type DeepPartial<T> = {
|
||||||
[P in keyof T]?: DeepPartial<T[P]>;
|
[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",
|
"management",
|
||||||
"features",
|
"features",
|
||||||
"savedObjects",
|
"savedObjects",
|
||||||
"share"
|
"share",
|
||||||
|
"triggersActionsUi",
|
||||||
|
"fieldFormats"
|
||||||
],
|
],
|
||||||
"optionalPlugins": [
|
"optionalPlugins": [
|
||||||
"security",
|
"security",
|
||||||
"usageCollection"
|
"usageCollection",
|
||||||
|
"alerting"
|
||||||
],
|
],
|
||||||
"configPath": ["xpack", "transform"],
|
"configPath": ["xpack", "transform"],
|
||||||
"requiredBundles": [
|
"requiredBundles": [
|
||||||
|
@ -24,6 +27,9 @@
|
||||||
"kibanaReact",
|
"kibanaReact",
|
||||||
"ml"
|
"ml"
|
||||||
],
|
],
|
||||||
|
"extraPublicDirs": [
|
||||||
|
"common"
|
||||||
|
],
|
||||||
"owner": {
|
"owner": {
|
||||||
"name": "Machine Learning UI",
|
"name": "Machine Learning UI",
|
||||||
"githubTeam": "ml-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 = () => {
|
export const plugin = () => {
|
||||||
return new TransformUiPlugin();
|
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 { ManagementSetup } from 'src/plugins/management/public';
|
||||||
import type { SharePluginStart } from 'src/plugins/share/public';
|
import type { SharePluginStart } from 'src/plugins/share/public';
|
||||||
import { registerFeature } from './register_feature';
|
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 {
|
export interface PluginsDependencies {
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
|
@ -21,11 +24,13 @@ export interface PluginsDependencies {
|
||||||
home: HomePublicPluginSetup;
|
home: HomePublicPluginSetup;
|
||||||
savedObjects: SavedObjectsStart;
|
savedObjects: SavedObjectsStart;
|
||||||
share: SharePluginStart;
|
share: SharePluginStart;
|
||||||
|
alerting?: AlertingSetup;
|
||||||
|
triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TransformUiPlugin {
|
export class TransformUiPlugin {
|
||||||
public setup(coreSetup: CoreSetup<PluginsDependencies>, pluginsSetup: PluginsDependencies): void {
|
public setup(coreSetup: CoreSetup<PluginsDependencies>, pluginsSetup: PluginsDependencies): void {
|
||||||
const { management, home } = pluginsSetup;
|
const { management, home, triggersActionsUi } = pluginsSetup;
|
||||||
|
|
||||||
// Register management section
|
// Register management section
|
||||||
const esSection = management.sections.section.data;
|
const esSection = management.sections.section.data;
|
||||||
|
@ -41,6 +46,10 @@ export class TransformUiPlugin {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
registerFeature(home);
|
registerFeature(home);
|
||||||
|
|
||||||
|
if (triggersActionsUi) {
|
||||||
|
triggersActionsUi.ruleTypeRegistry.register(getTransformHealthRuleType());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public start() {}
|
public start() {}
|
||||||
|
|
|
@ -10,3 +10,5 @@ import { PluginInitializerContext } from 'src/core/server';
|
||||||
import { TransformServerPlugin } from './plugin';
|
import { TransformServerPlugin } from './plugin';
|
||||||
|
|
||||||
export const plugin = (ctx: PluginInitializerContext) => new TransformServerPlugin(ctx);
|
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 { Dependencies } from './types';
|
||||||
import { ApiRoutes } from './routes';
|
import { ApiRoutes } from './routes';
|
||||||
import { License } from './services';
|
import { License } from './services';
|
||||||
|
import { registerTransformHealthRuleType } from './lib/alerting';
|
||||||
|
|
||||||
const basicLicense: LicenseType = 'basic';
|
const basicLicense: LicenseType = 'basic';
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> {
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
{ http, getStartServices, elasticsearch }: CoreSetup,
|
{ http, getStartServices, elasticsearch }: CoreSetup,
|
||||||
{ licensing, features }: Dependencies
|
{ licensing, features, alerting }: Dependencies
|
||||||
): {} {
|
): {} {
|
||||||
const router = http.createRouter();
|
const router = http.createRouter();
|
||||||
|
|
||||||
|
@ -75,6 +76,10 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> {
|
||||||
license: this.license,
|
license: this.license,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (alerting) {
|
||||||
|
registerTransformHealthRuleType({ alerting, logger: this.logger });
|
||||||
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,12 @@ import { IRouter } from 'src/core/server';
|
||||||
import { LicensingPluginSetup } from '../../licensing/server';
|
import { LicensingPluginSetup } from '../../licensing/server';
|
||||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||||
import { License } from './services';
|
import { License } from './services';
|
||||||
|
import type { AlertingPlugin } from '../../alerting/server';
|
||||||
|
|
||||||
export interface Dependencies {
|
export interface Dependencies {
|
||||||
licensing: LicensingPluginSetup;
|
licensing: LicensingPluginSetup;
|
||||||
features: FeaturesPluginSetup;
|
features: FeaturesPluginSetup;
|
||||||
|
alerting?: AlertingPlugin['setup'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteDependencies {
|
export interface RouteDependencies {
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
|
||||||
loadTestFile(require.resolve('./alerts_space1'));
|
loadTestFile(require.resolve('./alerts_space1'));
|
||||||
loadTestFile(require.resolve('./alerts_default_space'));
|
loadTestFile(require.resolve('./alerts_default_space'));
|
||||||
loadTestFile(require.resolve('./builtin_alert_types'));
|
loadTestFile(require.resolve('./builtin_alert_types'));
|
||||||
|
loadTestFile(require.resolve('./transform_rule_types'));
|
||||||
loadTestFile(require.resolve('./mustache_templates.ts'));
|
loadTestFile(require.resolve('./mustache_templates.ts'));
|
||||||
loadTestFile(require.resolve('./notify_when'));
|
loadTestFile(require.resolve('./notify_when'));
|
||||||
loadTestFile(require.resolve('./ephemeral'));
|
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);
|
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) {
|
async createAndRunTransform(transformId: string, transformConfig: PutTransformsRequestSchema) {
|
||||||
await this.createTransform(transformId, transformConfig);
|
await this.createTransform(transformId, transformConfig);
|
||||||
await this.startTransform(transformId);
|
await this.startTransform(transformId);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue