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

[Logs UI] Create screen to set up analysis ML jobs #43413

Merged
merged 32 commits into from
Aug 22, 2019
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
de0499e
Add empty analysis tab
weltenwort Aug 8, 2019
8d06a54
Merge branch 'master' into logs-ui-add-analysis-tab
weltenwort Aug 8, 2019
0ba9cca
Add ml capabilities check
weltenwort Aug 8, 2019
84248c2
Add job status checking functionality
Kerry350 Aug 8, 2019
31be96e
Add a loading page for the job status check
Kerry350 Aug 9, 2019
6c0bac5
Change types / change method for deriving space ID / change setup req…
Kerry350 Aug 9, 2019
98a9fa9
Use new structure
Kerry350 Aug 9, 2019
83330d7
Add module setup to log analysis jobs hook
weltenwort Aug 9, 2019
916c167
Merge remote-tracking branch 'upstream/master' into logs-ui-add-ml-mo…
Kerry350 Aug 13, 2019
6951bde
Add ID to path
Kerry350 Aug 13, 2019
fe9c76a
Merge remote-tracking branch 'weltenwort/logs-ui-add-ml-module-setup-…
Zacqary Aug 15, 2019
705577a
[Logs UI] Add analyis setup landing screen
Zacqary Aug 15, 2019
1beb78c
Add function to set up ML module on click
Zacqary Aug 15, 2019
f6594e3
Use partial type for start and end props
Zacqary Aug 15, 2019
fb139df
Add start and end time selection
Zacqary Aug 15, 2019
c36586f
Fix syntax
Kerry350 Aug 16, 2019
2569444
Change seconds timestamp to ms
Zacqary Aug 16, 2019
c4a2c0a
Merge branch '41877-ml-setup-screen' of github.com:Zacqary/kibana int…
Zacqary Aug 16, 2019
fab784e
Update wording
Zacqary Aug 16, 2019
3104d10
Use FormControlLayout to clear datepickers
Zacqary Aug 16, 2019
7ec64cd
Update wording about earlier start date
Zacqary Aug 16, 2019
895cb3f
Remove specific point in time wording
Zacqary Aug 16, 2019
69d7a7b
Fix typechecking
Zacqary Aug 16, 2019
b20e018
Reload analysis page on successful job creation
Zacqary Aug 16, 2019
5ca200f
Add error handling for setup failure
Zacqary Aug 16, 2019
5bb9e35
Merge remote-tracking branch 'upstream/master' into 41877-ml-setup-sc…
Zacqary Aug 16, 2019
09efec9
Update description ton of feature to reflect 7.4 feature set
Zacqary Aug 19, 2019
4908922
Add toggleable default message
Zacqary Aug 19, 2019
c7a33dc
Revert to EuiFormControlLayout until eui changes are pushed
Zacqary Aug 19, 2019
4871bad
Merge branch 'master' of github.com:elastic/kibana into 41877-ml-setu…
Zacqary Aug 19, 2019
c13eb2f
Merge remote-tracking branch 'upstream/master' into 41877-ml-setup-sc…
Kerry350 Aug 22, 2019
41ca5e4
Remove sample data index if user has it set
Kerry350 Aug 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@

import { JobType } from './log_analysis';

export const bucketSpan = 900000;

export const getJobIdPrefix = (spaceId: string, sourceId: string) =>
`kibana-logs-ui-${spaceId}-${sourceId}-`;

export const getJobId = (spaceId: string, sourceId: string, jobType: JobType) =>
`${getJobIdPrefix(spaceId, sourceId)}${jobType}`;

export const getDatafeedId = (spaceId: string, sourceId: string, jobType: JobType) =>
`datafeed-${getJobId(spaceId, sourceId, jobType)}`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import * as rt from 'io-ts';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can someone explain to me why io-ts gets called rt?

import { kfetch } from 'ui/kfetch';

import { getJobIdPrefix } from '../../../../../common/log_analysis';
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';

const MODULE_ID = 'logs_ui_analysis';

export const callSetupMlModuleAPI = async (
start: number | undefined,
end: number | undefined,
spaceId: string,
sourceId: string,
indexPattern: string,
timeField: string,
bucketSpan: number
) => {
const response = await kfetch({
method: 'POST',
pathname: `/api/ml/modules/setup/${MODULE_ID}`,
body: JSON.stringify(
setupMlModuleRequestPayloadRT.encode({
start,
end,
indexPatternName: indexPattern,
prefix: getJobIdPrefix(spaceId, sourceId),
startDatafeed: true,
jobOverrides: [
{
job_id: 'log-entry-rate',
analysis_config: {
bucket_span: `${bucketSpan}ms`,
},
data_description: {
time_field: timeField,
},
},
],
datafeedOverrides: [
{
job_id: 'log-entry-rate',
aggregations: {
buckets: {
date_histogram: {
field: timeField,
fixed_interval: `${bucketSpan}ms`,
},
aggregations: {
[timeField]: {
max: {
field: `${timeField}`,
},
},
doc_count_per_minute: {
bucket_script: {
script: {
params: {
bucket_span_in_ms: bucketSpan,
},
},
},
},
},
},
},
},
],
})
),
});

return setupMlModuleResponsePayloadRT.decode(response).getOrElseL(throwErrors(createPlainError));
};

const setupMlModuleTimeParamsRT = rt.partial({
start: rt.number,
end: rt.number,
});

const setupMlModuleRequestParamsRT = rt.type({
indexPatternName: rt.string,
prefix: rt.string,
startDatafeed: rt.boolean,
jobOverrides: rt.array(rt.object),
datafeedOverrides: rt.array(rt.object),
});

const setupMlModuleRequestPayloadRT = rt.intersection([
setupMlModuleTimeParamsRT,
setupMlModuleRequestParamsRT,
]);

const setupMlModuleResponsePayloadRT = rt.type({
datafeeds: rt.array(
rt.type({
id: rt.string,
started: rt.boolean,
success: rt.boolean,
})
),
jobs: rt.array(
rt.type({
id: rt.string,
success: rt.boolean,
})
),
});

export type SetupMlModuleResponsePayload = rt.TypeOf<typeof setupMlModuleResponsePayloadRT>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,70 @@

import createContainer from 'constate-latest';
import { useMemo, useEffect, useState } from 'react';
import { values } from 'lodash';
import { getJobId } from '../../../../common/log_analysis';
import { bucketSpan, getJobId } from '../../../../common/log_analysis';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { callSetupMlModuleAPI, SetupMlModuleResponsePayload } from './api/ml_setup_module_api';
import { callJobsSummaryAPI } from './api/ml_get_jobs_summary_api';

type JobStatus = 'unknown' | 'closed' | 'closing' | 'failed' | 'opened' | 'opening' | 'deleted';
// type DatafeedStatus = 'unknown' | 'started' | 'starting' | 'stopped' | 'stopping' | 'deleted';
// combines and abstracts job and datafeed status
type JobStatus =
| 'unknown'
| 'missing'
| 'inconsistent'
| 'created'
| 'started'
| 'opening'
| 'opened';

export const useLogAnalysisJobs = ({
indexPattern,
sourceId,
spaceId,
timeField,
}: {
indexPattern: string;
sourceId: string;
spaceId: string;
timeField: string;
}) => {
const [jobStatus, setJobStatus] = useState<{
logEntryRate: JobStatus;
}>({
logEntryRate: 'unknown',
});

// const [setupMlModuleRequest, setupMlModule] = useTrackedPromise(
// {
// cancelPreviousOn: 'resolution',
// createPromise: async () => {
// kfetch({
// method: 'POST',
// pathname: '/api/ml/modules/setup',
// body: JSON.stringify(
// setupMlModuleRequestPayloadRT.encode({
// indexPatternName: indexPattern,
// prefix: getJobIdPrefix(spaceId, sourceId),
// startDatafeed: true,
// })
// ),
// });
// },
// },
// [indexPattern, spaceId, sourceId]
// );
const [setupMlModuleRequest, setupMlModule] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async (start, end) => {
return await callSetupMlModuleAPI(
start,
end,
spaceId,
sourceId,
indexPattern,
timeField,
bucketSpan
);
},
onResolve: ({ datafeeds, jobs }: SetupMlModuleResponsePayload) => {
const hasSuccessfullyCreatedJobs = jobs.every(job => job.success);
const hasSuccessfullyStartedDatafeeds = datafeeds.every(
datafeed => datafeed.success && datafeed.started
);

setJobStatus(currentJobStatus => ({
...currentJobStatus,
logEntryRate: hasSuccessfullyCreatedJobs
? hasSuccessfullyStartedDatafeeds
? 'started'
: 'created'
: 'inconsistent',
}));
},
},
[indexPattern, spaceId, sourceId]
);

const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise(
{
Expand Down Expand Up @@ -77,20 +99,34 @@ export const useLogAnalysisJobs = ({
}, []);

const isSetupRequired = useMemo(() => {
const jobStates = values(jobStatus);
const jobStates = Object.values(jobStatus);
return (
jobStates.filter(state => state === 'opened' || state === 'opening').length < jobStates.length
jobStates.filter(state => ['opened', 'opening', 'created', 'started'].includes(state))
.length < jobStates.length
);
}, [jobStatus]);

const isLoadingSetupStatus = useMemo(() => fetchJobStatusRequest.state === 'pending', [
fetchJobStatusRequest.state,
]);

const isSettingUpMlModule = useMemo(() => setupMlModuleRequest.state === 'pending', [
setupMlModuleRequest.state,
]);

const didSetupFail = useMemo(
() => !isSettingUpMlModule && setupMlModuleRequest.state !== 'uninitialized' && isSetupRequired,
[setupMlModuleRequest.state, jobStatus]
);

return {
jobStatus,
isSetupRequired,
isLoadingSetupStatus,
setupMlModule,
setupMlModuleRequest,
isSettingUpMlModule,
didSetupFail,
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useMemo, useState } from 'react';
import moment, { Moment } from 'moment';

import { i18n } from '@kbn/i18n';
import {
EuiForm,
EuiDescribedFormGroup,
EuiFormRow,
EuiDatePicker,
EuiFlexGroup,
EuiFormControlLayout,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { CreateMLJobsButton } from './create_ml_jobs_button';

const startTimeLabel = i18n.translate('xpack.infra.analysisSetup.startTimeLabel', {
defaultMessage: 'Start time',
});
const endTimeLabel = i18n.translate('xpack.infra.analysisSetup.endTimeLabel', {
defaultMessage: 'End time',
});
const startTimeDefaultDescription = i18n.translate(
'xpack.infra.analysisSetup.startTimeDefaultDescription',
{
defaultMessage: 'Start of log indices',
}
);
const endTimeDefaultDescription = i18n.translate(
'xpack.infra.analysisSetup.endTimeDefaultDescription',
{
defaultMessage: 'Indefinitely',
}
);

function selectedDateToParam(selectedDate: Moment | null) {
if (selectedDate) {
return selectedDate.valueOf(); // To ms unix timestamp
}
return undefined;
}

export const AnalysisSetupTimerangeForm: React.FunctionComponent<{
isSettingUp: boolean;
setupMlModule: (startTime: number | undefined, endTime: number | undefined) => Promise<any>;
}> = ({ isSettingUp, setupMlModule }) => {
const [startTime, setStartTime] = useState<Moment | null>(null);
const [endTime, setEndTime] = useState<Moment | null>(null);

const now = useMemo(() => moment(), []);
const selectedEndTimeIsToday = !endTime || endTime.isSame(now, 'day');

const onClickCreateJob = () =>
setupMlModule(selectedDateToParam(startTime), selectedDateToParam(endTime));

return (
<EuiForm>
<EuiDescribedFormGroup
idAria="timeRange"
title={
<FormattedMessage
id="xpack.infra.analysisSetup.timeRangeTitle"
defaultMessage="Choose a time range"
/>
}
description={
<FormattedMessage
id="xpack.infra.analysisSetup.timeRangeDescription"
defaultMessage="By default, Machine Learning analyzes log messages from the start of your log indices and continues indefinitely. You can specify a different date to begin, to end, or both."
/>
}
>
<EuiFormRow
describedByIds={['timeRange']}
error={false}
fullWidth
isInvalid={false}
label={startTimeLabel}
>
<EuiFlexGroup gutterSize="s">
<EuiFormControlLayout
clear={startTime ? { onClick: () => setStartTime(null) } : undefined}
>
<EuiDatePicker
showTimeSelect
selected={startTime}
onChange={setStartTime}
placeholder={startTimeDefaultDescription}
maxDate={now}
/>
</EuiFormControlLayout>
</EuiFlexGroup>
</EuiFormRow>
<EuiFormRow
describedByIds={['timeRange']}
error={false}
fullWidth
isInvalid={false}
label={endTimeLabel}
>
<EuiFlexGroup gutterSize="s">
<EuiFormControlLayout clear={endTime ? { onClick: () => setEndTime(null) } : undefined}>
<EuiDatePicker
showTimeSelect
selected={endTime}
onChange={setEndTime}
placeholder={endTimeDefaultDescription}
openToDate={now}
minDate={now}
minTime={
selectedEndTimeIsToday
? now
: moment()
.hour(0)
.minutes(0)
}
maxTime={moment()
.hour(23)
.minutes(59)}
/>
</EuiFormControlLayout>
</EuiFlexGroup>
</EuiFormRow>
<CreateMLJobsButton isLoading={isSettingUp} onClick={onClickCreateJob} />
</EuiDescribedFormGroup>
</EuiForm>
);
};
Loading