Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Delayed data test for Anomaly Detection jobs health rule type #107183

Merged
merged 32 commits into from Aug 5, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bea342b
[ML] enable test selection
darnautov Jul 29, 2021
e4aee46
[ML] executor update for annotations
darnautov Jul 29, 2021
8e2c5d5
[ML] update unit tests
darnautov Jul 30, 2021
6979ad4
[ML] fix i18n
darnautov Jul 30, 2021
79e6bdf
[ML] update schema
darnautov Jul 30, 2021
856963a
[ML] fix ts
darnautov Jul 30, 2021
2979454
[ML] account for docs count, update unit tests
darnautov Aug 2, 2021
8bc164e
[ML] update translation strings
darnautov Aug 2, 2021
a81a180
Merge remote-tracking branch 'upstream/master' into ml-101028-delayed…
darnautov Aug 2, 2021
d53c91b
[ML] add types
darnautov Aug 2, 2021
ae257ea
[ML] fetch the latest annotation sorted by modified_time
darnautov Aug 3, 2021
ca818b7
Merge remote-tracking branch 'upstream/master' into ml-101028-delayed…
darnautov Aug 3, 2021
e4152e0
[ML] getDelayedDataAnnotations
darnautov Aug 3, 2021
d5a8aec
[ML] update unit tests
darnautov Aug 3, 2021
13ca0cf
[ML] set default number of docs to 1, update schema validation
darnautov Aug 3, 2021
eb66a68
[ML] getDelayedDataLookbackTimestamp
darnautov Aug 4, 2021
a4e0bc5
[ML] filter null values, update unit tests
darnautov Aug 4, 2021
cc7128e
[ML] account for query delay, refactor with memoize
darnautov Aug 4, 2021
4de790d
[ML] update unit test
darnautov Aug 4, 2021
164ec64
[ML] remove previousStartedAt
darnautov Aug 4, 2021
ca44946
[ML] filter based on the job config
darnautov Aug 4, 2021
1e8d5d5
[ML] fix tests
darnautov Aug 4, 2021
ef5130c
[ML] add maps
darnautov Aug 4, 2021
9606a2f
[ML] combine filters
darnautov Aug 4, 2021
637cc90
[ML] move range query inside of a filter
darnautov Aug 4, 2021
66e331a
[ML] filter out jobs with missing datafeed
darnautov Aug 4, 2021
0968256
[ML] resolveLookbackInterval only from jobs with datafeeds
darnautov Aug 4, 2021
c57b07b
[ML] do not show an error on empty time interval
darnautov Aug 5, 2021
d6e3665
[ML] add help tooltips
darnautov Aug 5, 2021
52fcd40
Merge remote-tracking branch 'upstream/master' into ml-101028-delayed…
darnautov Aug 5, 2021
3afb856
[ML] update description for the datafeed check
darnautov Aug 5, 2021
83d90ed
[ML] update default message
darnautov Aug 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 51 additions & 20 deletions x-pack/plugins/ml/common/constants/alerts.ts
Expand Up @@ -21,26 +21,57 @@ export const TOP_N_BUCKETS_COUNT = 1;

export const ALL_JOBS_SELECTION = '*';

export const HEALTH_CHECK_NAMES: Record<JobsHealthTests, string> = {
datafeed: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedCheckName', {
defaultMessage: 'Datafeed is not started',
}),
mml: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlCheckName', {
defaultMessage: 'Model memory limit reached',
}),
errorMessages: i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesCheckName',
{
export const HEALTH_CHECK_NAMES: Record<JobsHealthTests, { name: string; description: string }> = {
datafeed: {
name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedCheckName', {
defaultMessage: 'Datafeed is not started',
}),
description: i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedCheckDescription',
{
defaultMessage: 'Datafeed is not started',
darnautov marked this conversation as resolved.
Show resolved Hide resolved
}
),
},
mml: {
name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlCheckName', {
defaultMessage: 'Model memory limit reached',
}),
description: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlCheckDescription', {
defaultMessage: 'Get alerted when job reaches soft or hard model memory limit.',
}),
},
delayedData: {
name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataCheckName', {
defaultMessage: 'Data delay has occurred',
}),
description: i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataCheckDescription',
{
defaultMessage: 'Get alerted if a job has been identified as suffering from delayed data.',
darnautov marked this conversation as resolved.
Show resolved Hide resolved
}
),
},
errorMessages: {
name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesCheckName', {
defaultMessage: 'There are errors in the job messages',
}
),
behindRealtime: i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.behindRealtimeCheckName',
{
}),
description: i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesCheckDescription',
{
defaultMessage: 'There are errors in the job messages',
}
),
},
behindRealtime: {
name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.behindRealtimeCheckName', {
defaultMessage: 'Job is running behind real-time',
}
),
delayedData: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataCheckName', {
defaultMessage: 'Data delay has occurred',
}),
}),
description: i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.behindRealtimeCheckDescription',
{
defaultMessage: 'Job is running behind real-time',
}
),
},
};
2 changes: 1 addition & 1 deletion x-pack/plugins/ml/common/types/alerts.ts
Expand Up @@ -109,7 +109,7 @@ export interface JobAlertingRuleStats {
alerting_rules?: MlAnomalyDetectionAlertRule[];
}

interface CommonHealthCheckConfig {
export interface CommonHealthCheckConfig {
enabled: boolean;
}

Expand Down
7 changes: 6 additions & 1 deletion x-pack/plugins/ml/common/types/annotations.ts
Expand Up @@ -86,7 +86,12 @@ export interface Annotation {
annotation: string;
job_id: string;
type: ANNOTATION_TYPE.ANNOTATION | ANNOTATION_TYPE.COMMENT;
event?: string;
event?:
| 'user'
| 'delayed_data'
| 'model_snapshot_stored'
| 'model_change'
| 'categorization_status_change';
detector_index?: number;
partition_field_name?: string;
partition_field_value?: string;
Expand Down
14 changes: 14 additions & 0 deletions x-pack/plugins/ml/common/util/alerts.test.ts
Expand Up @@ -90,13 +90,22 @@ describe('getResultJobsHealthRuleConfig', () => {
mml: {
enabled: true,
},
delayedData: {
docsCount: 0,
enabled: true,
timeInterval: null,
},
});
});
test('returns config with overridden values based on provided configuration', () => {
expect(
getResultJobsHealthRuleConfig({
mml: { enabled: false },
errorMessages: { enabled: true },
delayedData: {
enabled: true,
docsCount: 1,
},
})
).toEqual({
datafeed: {
Expand All @@ -105,6 +114,11 @@ describe('getResultJobsHealthRuleConfig', () => {
mml: {
enabled: false,
},
delayedData: {
docsCount: 1,
enabled: true,
timeInterval: null,
},
});
});
});
4 changes: 3 additions & 1 deletion x-pack/plugins/ml/common/util/alerts.ts
Expand Up @@ -54,7 +54,7 @@ export function getTopNBuckets(job: Job): number {
return Math.ceil(narrowBucketLength / bucketSpan.asSeconds());
}

const implementedTests = ['datafeed', 'mml'] as JobsHealthTests[];
const implementedTests = ['datafeed', 'mml', 'delayedData'] as JobsHealthTests[];

/**
* Returns tests configuration combined with default values.
Expand All @@ -70,6 +70,8 @@ export function getResultJobsHealthRuleConfig(config: JobsHealthRuleTestsConfig)
},
delayedData: {
enabled: config?.delayedData?.enabled ?? true,
docsCount: config?.delayedData?.docsCount ?? 0,
timeInterval: config?.delayedData?.timeInterval ?? null,
},
behindRealtime: {
enabled: config?.behindRealtime?.enabled ?? true,
Expand Down
Expand Up @@ -12,6 +12,7 @@ import { PluginSetupContract as AlertingSetup } from '../../../../alerting/publi
import { ML_ALERT_TYPES } from '../../../common/constants/alerts';
import { MlAnomalyDetectionJobsHealthRuleParams } from '../../../common/types/alerts';
import { getResultJobsHealthRuleConfig } from '../../../common/util/alerts';
import { validateLookbackInterval } from '../validators';

export function registerJobsHealthAlertingRule(
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup,
Expand All @@ -32,6 +33,7 @@ export function registerJobsHealthAlertingRule(
errors: {
includeJobs: new Array<string>(),
testsConfig: new Array<string>(),
delayedData: new Array<string>(),
} as Record<keyof MlAnomalyDetectionJobsHealthRuleParams, string[]>,
};

Expand All @@ -53,6 +55,31 @@ export function registerJobsHealthAlertingRule(
);
}

if (
!!resultTestConfig.delayedData.timeInterval &&
validateLookbackInterval(resultTestConfig.delayedData.timeInterval)
darnautov marked this conversation as resolved.
Show resolved Hide resolved
) {
validationResult.errors.delayedData.push(
i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.testsConfig.delayedData.timeIntervalErrorMessage',
{
defaultMessage: 'Time interval is invalid',
darnautov marked this conversation as resolved.
Show resolved Hide resolved
}
)
);
}

if (resultTestConfig.delayedData.docsCount === 0) {
darnautov marked this conversation as resolved.
Show resolved Hide resolved
validationResult.errors.delayedData.push(
i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.testsConfig.delayedData.docsCountErrorMessage',
{
defaultMessage: 'Number of documents is invalid',
darnautov marked this conversation as resolved.
Show resolved Hide resolved
}
)
);
}

return validationResult;
},
requiresAppContext: false,
Expand Down
Expand Up @@ -5,66 +5,121 @@
* 2.0.
*/

import React, { FC, Fragment, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormFieldset, EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui';
import React, { FC, useCallback } from 'react';
import {
EuiDescribedFormGroup,
EuiFieldNumber,
EuiForm,
EuiFormRow,
EuiSpacer,
EuiSwitch,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { JobsHealthRuleTestsConfig, JobsHealthTests } from '../../../common/types/alerts';
import { getResultJobsHealthRuleConfig } from '../../../common/util/alerts';
import { HEALTH_CHECK_NAMES } from '../../../common/constants/alerts';
import { TimeIntervalControl } from '../time_interval_control';

interface TestsSelectionControlProps {
config: JobsHealthRuleTestsConfig;
onChange: (update: JobsHealthRuleTestsConfig) => void;
errors?: string[];
}

export const TestsSelectionControl: FC<TestsSelectionControlProps> = ({
config,
onChange,
errors,
}) => {
const uiConfig = getResultJobsHealthRuleConfig(config);
export const TestsSelectionControl: FC<TestsSelectionControlProps> = React.memo(
({ config, onChange, errors }) => {
const uiConfig = getResultJobsHealthRuleConfig(config);

const updateCallback = useCallback(
(update: Partial<Exclude<JobsHealthRuleTestsConfig, undefined>>) => {
onChange({
...(config ?? {}),
...update,
});
},
[onChange, config]
);
const updateCallback = useCallback(
(update: Partial<Exclude<JobsHealthRuleTestsConfig, undefined>>) => {
onChange({
...(config ?? {}),
...update,
});
},
[onChange, config]
);

return (
<EuiFormFieldset
legend={{
children: i18n.translate(
'xpack.ml.alertTypes.jobsHealthAlertingRule.testsSelection.legend',
{
defaultMessage: 'Select health checks to perform',
}
),
}}
>
{Object.entries(uiConfig).map(([name, conf], i) => {
return (
<Fragment key={name}>
<EuiFormRow
isInvalid={Object.keys(uiConfig).length === i + 1 && !!errors?.length}
error={errors}
return (
<EuiForm component="div" isInvalid={!!errors?.length} error={errors}>
{(Object.entries(uiConfig) as Array<
[JobsHealthTests, typeof uiConfig[JobsHealthTests]]
>).map(([name, conf], i) => {
return (
<EuiDescribedFormGroup
key={name}
title={<h4>{HEALTH_CHECK_NAMES[name]?.name}</h4>}
description={HEALTH_CHECK_NAMES[name]?.description}
>
<EuiSwitch
label={HEALTH_CHECK_NAMES[name as JobsHealthTests]}
onChange={updateCallback.bind(null, {
[name]: { enabled: !uiConfig[name as JobsHealthTests].enabled },
})}
checked={uiConfig[name as JobsHealthTests].enabled}
/>
</EuiFormRow>
<EuiSpacer size="s" />
</Fragment>
);
})}
</EuiFormFieldset>
);
};
<EuiFormRow>
<EuiSwitch
label={
<FormattedMessage
id="xpack.ml.alertTypes.jobsHealthAlertingRule.testsSelection.enableTestLabel"
defaultMessage="Enable"
/>
}
onChange={updateCallback.bind(null, {
[name]: {
...uiConfig[name],
enabled: !uiConfig[name].enabled,
},
})}
checked={uiConfig[name].enabled}
/>
</EuiFormRow>
<EuiSpacer size="s" />

{name === 'delayedData' ? (
<>
<EuiFormRow
label={
<FormattedMessage
id="xpack.ml.alertTypes.jobsHealthAlertingRule.testsSelection.delayedData.docsCountLabel"
defaultMessage="Number of documents"
/>
}
>
<EuiFieldNumber
value={uiConfig.delayedData.docsCount}
onChange={(e) => {
updateCallback({
[name]: {
...uiConfig[name],
docsCount: Number(e.target.value),
},
});
}}
/>
</EuiFormRow>

<EuiSpacer size="s" />

<TimeIntervalControl
label={
<FormattedMessage
id="xpack.ml.alertTypes.jobsHealthAlertingRule.testsSelection.delayedData.timeIntervalLabel"
defaultMessage="Time interval"
/>
}
value={uiConfig.delayedData.timeInterval}
onChange={(e) => {
updateCallback({
[name]: {
...uiConfig[name],
timeInterval: e,
},
});
}}
/>

<EuiSpacer size="s" />
</>
) : null}
</EuiDescribedFormGroup>
);
})}
</EuiForm>
);
}
);