Skip to content

Commit 4cd02c2

Browse files
authored
[7.x] [Logs UI] Add ML job status callouts to results page (#4… (#48309)
Backports the following commits to 7.x: - [Logs UI] Add ML job status callouts to results page (#47642)
1 parent e51369a commit 4cd02c2

21 files changed

+721
-150
lines changed

x-pack/legacy/plugins/infra/common/log_analysis/log_analysis.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,45 @@ export const jobTypeRT = rt.keyof({
1212

1313
export type JobType = rt.TypeOf<typeof jobTypeRT>;
1414

15-
export const jobStatusRT = rt.keyof({
16-
created: null,
17-
missing: null,
18-
running: null,
19-
});
15+
// combines and abstracts job and datafeed status
16+
export type JobStatus =
17+
| 'unknown'
18+
| 'missing'
19+
| 'initializing'
20+
| 'stopped'
21+
| 'started'
22+
| 'finished'
23+
| 'failed';
24+
25+
export type SetupStatus =
26+
| 'initializing' // acquiring job statuses to determine setup status
27+
| 'unknown' // job status could not be acquired (failed request etc)
28+
| 'required' // jobs are missing
29+
| 'requiredForReconfiguration' // the configurations don't match the source configurations
30+
| 'requiredForUpdate' // the definitions don't match the module definitions
31+
| 'pending' // In the process of setting up the module for the first time or retrying, waiting for response
32+
| 'succeeded' // setup succeeded, notifying user
33+
| 'failed' // setup failed, notifying user
34+
| 'hiddenAfterSuccess' // hide the setup screen and we show the results for the first time
35+
| 'skipped' // setup hidden because the module is in a correct state already
36+
| 'skippedButReconfigurable' // setup hidden even though the job configurations are outdated
37+
| 'skippedButUpdatable'; // setup hidden even though the job definitions are outdated
38+
39+
/**
40+
* Maps a job status to the possibility that results have already been produced
41+
* before this state was reached.
42+
*/
43+
export const isJobStatusWithResults = (jobStatus: JobStatus) =>
44+
['started', 'finished', 'stopped', 'failed'].includes(jobStatus);
45+
46+
export const isHealthyJobStatus = (jobStatus: JobStatus) =>
47+
['started', 'finished'].includes(jobStatus);
2048

21-
export type JobStatus = rt.TypeOf<typeof jobStatusRT>;
49+
/**
50+
* Maps a setup status to the possibility that results have already been
51+
* produced before this state was reached.
52+
*/
53+
export const isSetupStatusWithResults = (setupStatus: SetupStatus) =>
54+
['skipped', 'hiddenAfterSuccess', 'skippedButReconfigurable', 'skippedButUpdatable'].includes(
55+
setupStatus
56+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export * from './log_analysis_job_problem_indicator';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { i18n } from '@kbn/i18n';
8+
import { FormattedMessage } from '@kbn/i18n/react';
9+
import React from 'react';
10+
11+
import { RecreateJobCallout } from './recreate_job_callout';
12+
13+
export const JobConfigurationOutdatedCallout: React.FC<{
14+
onRecreateMlJob: () => void;
15+
}> = ({ onRecreateMlJob }) => (
16+
<RecreateJobCallout title={jobConfigurationOutdatedTitle} onRecreateMlJob={onRecreateMlJob}>
17+
<FormattedMessage
18+
id="xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutMessage"
19+
defaultMessage="The ML job was created using a different source configuration. Recreate the job to apply the current configuration. This removes previously detected anomalies."
20+
/>
21+
</RecreateJobCallout>
22+
);
23+
24+
const jobConfigurationOutdatedTitle = i18n.translate(
25+
'xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutTitle',
26+
{
27+
defaultMessage: 'ML job configuration outdated',
28+
}
29+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { i18n } from '@kbn/i18n';
8+
import { FormattedMessage } from '@kbn/i18n/react';
9+
import React from 'react';
10+
11+
import { RecreateJobCallout } from './recreate_job_callout';
12+
13+
export const JobDefinitionOutdatedCallout: React.FC<{
14+
onRecreateMlJob: () => void;
15+
}> = ({ onRecreateMlJob }) => (
16+
<RecreateJobCallout title={jobDefinitionOutdatedTitle} onRecreateMlJob={onRecreateMlJob}>
17+
<FormattedMessage
18+
id="xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutMessage"
19+
defaultMessage="A newer version of the ML job is available. Recreate the job to deploy the newer version. This removes previously detected anomalies."
20+
/>
21+
</RecreateJobCallout>
22+
);
23+
24+
const jobDefinitionOutdatedTitle = i18n.translate(
25+
'xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutTitle',
26+
{
27+
defaultMessage: 'ML job definition outdated',
28+
}
29+
);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { EuiCallOut } from '@elastic/eui';
9+
import { i18n } from '@kbn/i18n';
10+
import { FormattedMessage } from '@kbn/i18n/react';
11+
12+
export const JobStoppedCallout: React.FC = () => (
13+
<EuiCallOut color="primary" iconType="pause" title={jobStoppedTitle}>
14+
<FormattedMessage
15+
id="xpack.infra.logs.analysis.jobStoppedCalloutMessage"
16+
defaultMessage="The ML job has been stopped manually or due to a lack of resources. New log entries will not be processed until the job has been restarted."
17+
tagName="p"
18+
/>
19+
</EuiCallOut>
20+
);
21+
22+
const jobStoppedTitle = i18n.translate('xpack.infra.logs.analysis.jobStoppedCalloutTitle', {
23+
defaultMessage: 'ML job stopped',
24+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
9+
import { JobStatus, SetupStatus } from '../../../../common/log_analysis';
10+
import { JobConfigurationOutdatedCallout } from './job_configuration_outdated_callout';
11+
import { JobDefinitionOutdatedCallout } from './job_definition_outdated_callout';
12+
import { JobStoppedCallout } from './job_stopped_callout';
13+
14+
export const LogAnalysisJobProblemIndicator: React.FC<{
15+
jobStatus: JobStatus;
16+
setupStatus: SetupStatus;
17+
onRecreateMlJobForReconfiguration: () => void;
18+
onRecreateMlJobForUpdate: () => void;
19+
}> = ({ jobStatus, setupStatus, onRecreateMlJobForReconfiguration, onRecreateMlJobForUpdate }) => {
20+
if (jobStatus === 'stopped') {
21+
return <JobStoppedCallout />;
22+
} else if (setupStatus === 'skippedButUpdatable') {
23+
return <JobDefinitionOutdatedCallout onRecreateMlJob={onRecreateMlJobForUpdate} />;
24+
} else if (setupStatus === 'skippedButReconfigurable') {
25+
return <JobConfigurationOutdatedCallout onRecreateMlJob={onRecreateMlJobForReconfiguration} />;
26+
}
27+
28+
return null; // no problem to indicate
29+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { EuiCallOut, EuiButton } from '@elastic/eui';
9+
import { FormattedMessage } from '@kbn/i18n/react';
10+
11+
export const RecreateJobCallout: React.FC<{
12+
onRecreateMlJob: () => void;
13+
title?: React.ReactNode;
14+
}> = ({ children, onRecreateMlJob, title }) => (
15+
<EuiCallOut color="warning" iconType="alert" title={title}>
16+
<p>{children}</p>
17+
<EuiButton color="warning" onClick={onRecreateMlJob}>
18+
<FormattedMessage
19+
id="xpack.infra.logs.analysis.recreateJobButtonLabel"
20+
defaultMessage="Recreate ML job"
21+
/>
22+
</EuiButton>
23+
</EuiCallOut>
24+
);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import * as rt from 'io-ts';
8+
9+
export const jobCustomSettingsRT = rt.partial({
10+
job_revision: rt.number,
11+
logs_source_config: rt.partial({
12+
indexPattern: rt.string,
13+
timestampField: rt.string,
14+
bucketSpan: rt.number,
15+
}),
16+
});

x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import * as rt from 'io-ts';
8-
import { kfetch } from 'ui/kfetch';
97
import { pipe } from 'fp-ts/lib/pipeable';
108
import { fold } from 'fp-ts/lib/Either';
119
import { identity } from 'fp-ts/lib/function';
10+
import * as rt from 'io-ts';
11+
import { kfetch } from 'ui/kfetch';
12+
13+
import { jobCustomSettingsRT } from './ml_api_types';
1214
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
1315
import { getAllModuleJobIds } from '../../../../../common/log_analysis';
1416

@@ -56,11 +58,14 @@ export const jobSummaryRT = rt.intersection([
5658
datafeedIndices: rt.array(rt.string),
5759
datafeedState: datafeedStateRT,
5860
fullJob: rt.partial({
61+
custom_settings: jobCustomSettingsRT,
5962
finished_time: rt.number,
6063
}),
6164
}),
6265
]);
6366

67+
export type JobSummary = rt.TypeOf<typeof jobSummaryRT>;
68+
6469
export const fetchJobStatusResponsePayloadRT = rt.array(jobSummaryRT);
6570

6671
export type FetchJobStatusResponsePayload = rt.TypeOf<typeof fetchJobStatusResponsePayloadRT>;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { fold } from 'fp-ts/lib/Either';
8+
import { pipe } from 'fp-ts/lib/pipeable';
9+
import { identity } from 'fp-ts/lib/function';
10+
import * as rt from 'io-ts';
11+
import { kfetch } from 'ui/kfetch';
12+
13+
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
14+
import { jobCustomSettingsRT } from './ml_api_types';
15+
16+
export const callGetMlModuleAPI = async (moduleId: string) => {
17+
const response = await kfetch({
18+
method: 'GET',
19+
pathname: `/api/ml/modules/get_module/${moduleId}`,
20+
});
21+
22+
return pipe(
23+
getMlModuleResponsePayloadRT.decode(response),
24+
fold(throwErrors(createPlainError), identity)
25+
);
26+
};
27+
28+
const jobDefinitionRT = rt.type({
29+
id: rt.string,
30+
config: rt.type({
31+
custom_settings: jobCustomSettingsRT,
32+
}),
33+
});
34+
35+
export type JobDefinition = rt.TypeOf<typeof jobDefinitionRT>;
36+
37+
const getMlModuleResponsePayloadRT = rt.type({
38+
id: rt.string,
39+
jobs: rt.array(jobDefinitionRT),
40+
});
41+
42+
export type GetMlModuleResponsePayload = rt.TypeOf<typeof getMlModuleResponsePayloadRT>;

x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,18 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import * as rt from 'io-ts';
8-
import { kfetch } from 'ui/kfetch';
9-
107
import { fold } from 'fp-ts/lib/Either';
118
import { pipe } from 'fp-ts/lib/pipeable';
129
import { identity } from 'fp-ts/lib/function';
10+
import * as rt from 'io-ts';
11+
import { kfetch } from 'ui/kfetch';
12+
1313
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
1414
import { getJobIdPrefix } from '../../../../../common/log_analysis';
15-
16-
const MODULE_ID = 'logs_ui_analysis';
17-
18-
// This is needed due to: https://github.com/elastic/kibana/issues/43671
19-
const removeSampleDataIndex = (indexPattern: string) => {
20-
const SAMPLE_DATA_INDEX = 'kibana_sample_data_logs*';
21-
return indexPattern
22-
.split(',')
23-
.filter(index => index !== SAMPLE_DATA_INDEX)
24-
.join(',');
25-
};
15+
import { jobCustomSettingsRT } from './ml_api_types';
2616

2717
export const callSetupMlModuleAPI = async (
18+
moduleId: string,
2819
start: number | undefined,
2920
end: number | undefined,
3021
spaceId: string,
@@ -35,23 +26,30 @@ export const callSetupMlModuleAPI = async (
3526
) => {
3627
const response = await kfetch({
3728
method: 'POST',
38-
pathname: `/api/ml/modules/setup/${MODULE_ID}`,
29+
pathname: `/api/ml/modules/setup/${moduleId}`,
3930
body: JSON.stringify(
4031
setupMlModuleRequestPayloadRT.encode({
4132
start,
4233
end,
43-
indexPatternName: removeSampleDataIndex(indexPattern),
34+
indexPatternName: indexPattern,
4435
prefix: getJobIdPrefix(spaceId, sourceId),
4536
startDatafeed: true,
4637
jobOverrides: [
4738
{
48-
job_id: 'log-entry-rate',
39+
job_id: 'log-entry-rate' as const,
4940
analysis_config: {
5041
bucket_span: `${bucketSpan}ms`,
5142
},
5243
data_description: {
5344
time_field: timeField,
5445
},
46+
custom_settings: {
47+
logs_source_config: {
48+
indexPattern,
49+
timestampField: timeField,
50+
bucketSpan,
51+
},
52+
},
5553
},
5654
],
5755
datafeedOverrides: [],
@@ -70,11 +68,22 @@ const setupMlModuleTimeParamsRT = rt.partial({
7068
end: rt.number,
7169
});
7270

71+
const setupMlModuleLogEntryRateJobOverridesRT = rt.type({
72+
job_id: rt.literal('log-entry-rate'),
73+
analysis_config: rt.type({
74+
bucket_span: rt.string,
75+
}),
76+
data_description: rt.type({
77+
time_field: rt.string,
78+
}),
79+
custom_settings: jobCustomSettingsRT,
80+
});
81+
7382
const setupMlModuleRequestParamsRT = rt.type({
7483
indexPatternName: rt.string,
7584
prefix: rt.string,
7685
startDatafeed: rt.boolean,
77-
jobOverrides: rt.array(rt.object),
86+
jobOverrides: rt.array(setupMlModuleLogEntryRateJobOverridesRT),
7887
datafeedOverrides: rt.array(rt.object),
7988
});
8089

0 commit comments

Comments
 (0)