Skip to content

Commit

Permalink
[Logs UI] Enhance analysis setup flow (#44990) (#45777)
Browse files Browse the repository at this point in the history
* Enhance setup with a steps style flow
  • Loading branch information
Kerry350 authored Sep 16, 2019
1 parent d421a2c commit f246c4f
Show file tree
Hide file tree
Showing 13 changed files with 572 additions and 259 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

import * as rt from 'io-ts';
import { kfetch } from 'ui/kfetch';
import { getJobId } from '../../../../../common/log_analysis';
import { getJobId, getDatafeedId } from '../../../../../common/log_analysis';

export const callCleanupMLResources = async (spaceId: string, sourceId: string) => {
export const callDeleteJobs = async (spaceId: string, sourceId: string) => {
// NOTE: Deleting the jobs via this API will delete the datafeeds at the same time
const deleteJobsResponse = await kfetch({
method: 'POST',
Expand All @@ -20,15 +20,19 @@ export const callCleanupMLResources = async (spaceId: string, sourceId: string)
),
});

// If for some reason we do need to delete datafeeds
// const deleteLogRateDatafeedResponse = await kfetch({
// method: 'DELETE',
// pathname: `/api/ml/datafeeds/${getDatafeedId(spaceId, sourceId, 'log-entry-rate')}`,
// });

return deleteJobsResponse;
};

export const callStopDatafeed = async (spaceId: string, sourceId: string) => {
// Stop datafeed due to https://github.com/elastic/kibana/issues/44652
const stopDatafeedResponse = await kfetch({
method: 'POST',
pathname: `/api/ml/datafeeds/${getDatafeedId(spaceId, sourceId, 'log-entry-rate')}/_stop`,
});

return stopDatafeedResponse;
};

export const deleteJobsRequestPayloadRT = rt.type({
jobIds: rt.array(rt.string),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './log_analysis_cleanup';
export * from './log_analysis_jobs';
export * from './log_analysis_results';
export * from './log_analysis_results_url_state';
export * from './log_analysis_status_state';
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import createContainer from 'constate-latest';
import { useMemo } from 'react';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { callCleanupMLResources } from './api/ml_cleanup_everything';
import { callDeleteJobs, callStopDatafeed } from './api/ml_cleanup';

export const useLogAnalysisCleanup = ({
sourceId,
Expand All @@ -20,7 +20,17 @@ export const useLogAnalysisCleanup = ({
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
return await callCleanupMLResources(spaceId, sourceId);
try {
await callStopDatafeed(spaceId, sourceId);
} catch (err) {
// Datefeed has been deleted / doesn't exist, proceed with deleting jobs anyway
if (err && err.res && err.res.status === 404) {
return await callDeleteJobs(spaceId, sourceId);
} else {
throw err;
}
}
return await callDeleteJobs(spaceId, sourceId);
},
},
[spaceId, sourceId]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,13 @@
*/

import createContainer from 'constate-latest';
import { useEffect, useMemo, useState } from 'react';

import { bucketSpan, getDatafeedId, getJobId, JobType } from '../../../../common/log_analysis';
import { useEffect, useMemo, useCallback } from 'react';
import { bucketSpan } from '../../../../common/log_analysis';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { callJobsSummaryAPI, FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api';
import { callJobsSummaryAPI } from './api/ml_get_jobs_summary_api';
import { callSetupMlModuleAPI, SetupMlModuleResponsePayload } from './api/ml_setup_module_api';

// combines and abstracts job and datafeed status
type JobStatus =
| 'unknown'
| 'missing'
| 'initializing'
| 'stopped'
| 'started'
| 'finished'
| 'failed';
import { useLogAnalysisCleanup } from './log_analysis_cleanup';
import { useStatusState } from './log_analysis_status_state';

export const useLogAnalysisJobs = ({
indexPattern,
Expand All @@ -33,19 +24,14 @@ export const useLogAnalysisJobs = ({
spaceId: string;
timeField: string;
}) => {
const [jobStatus, setJobStatus] = useState<Record<JobType, JobStatus>>({
'log-entry-rate': 'unknown',
});
const [hasCompletedSetup, setHasCompletedSetup] = useState<boolean>(false);
const { cleanupMLResources } = useLogAnalysisCleanup({ sourceId, spaceId });
const [statusState, dispatch] = useStatusState();

const [setupMlModuleRequest, setupMlModule] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async (start, end) => {
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate': 'initializing',
}));
dispatch({ type: 'startedSetup' });
return await callSetupMlModuleAPI(
start,
end,
Expand All @@ -57,46 +43,27 @@ export const useLogAnalysisJobs = ({
);
},
onResolve: ({ datafeeds, jobs }: SetupMlModuleResponsePayload) => {
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate':
hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, 'log-entry-rate'))(jobs) &&
hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, 'log-entry-rate'))(
datafeeds
)
? 'started'
: 'failed',
}));

setHasCompletedSetup(true);
dispatch({ type: 'finishedSetup', datafeeds, jobs, spaceId, sourceId });
},
onReject: () => {
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate': 'failed',
}));
dispatch({ type: 'failedSetup' });
},
},
[indexPattern, spaceId, sourceId]
[indexPattern, spaceId, sourceId, timeField, bucketSpan]
);

const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
dispatch({ type: 'fetchingJobStatuses' });
return await callJobsSummaryAPI(spaceId, sourceId);
},
onResolve: response => {
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate': getJobStatus(getJobId(spaceId, sourceId, 'log-entry-rate'))(response),
}));
dispatch({ type: 'fetchedJobStatuses', payload: response, spaceId, sourceId });
},
onReject: err => {
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate': 'unknown',
}));
dispatch({ type: 'failedFetchingJobStatuses' });
},
},
[indexPattern, spaceId, sourceId]
Expand All @@ -106,82 +73,37 @@ export const useLogAnalysisJobs = ({
fetchJobStatus();
}, []);

const isSetupRequired = useMemo(() => {
return !Object.values(jobStatus).every(state => ['started', 'finished'].includes(state));
}, [jobStatus]);

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

const isSettingUpMlModule = useMemo(() => setupMlModuleRequest.state === 'pending', [
setupMlModuleRequest.state,
]);
const viewResults = useCallback(() => {
dispatch({ type: 'viewedResults' });
}, []);

const didSetupFail = useMemo(() => {
const jobStates = Object.values(jobStatus);
return jobStates.filter(state => state === 'failed').length > 0;
}, [jobStatus]);
const retry = useCallback(
(start, end) => {
dispatch({ type: 'startedSetup' });
cleanupMLResources()
.then(() => {
setupMlModule(start, end);
})
.catch(() => {
dispatch({ type: 'failedSetup' });
});
},
[cleanupMLResources, setupMlModule]
);

return {
jobStatus,
isSetupRequired,
isLoadingSetupStatus,
setupMlModule,
setupMlModuleRequest,
isSettingUpMlModule,
didSetupFail,
hasCompletedSetup,
jobStatus: statusState.jobStatus,
isLoadingSetupStatus,
setup: setupMlModule,
retry,
setupStatus: statusState.setupStatus,
viewResults,
};
};

export const LogAnalysisJobs = createContainer(useLogAnalysisJobs);

const hasSuccessfullyCreatedJob = (jobId: string) => (
jobSetupResponses: SetupMlModuleResponsePayload['jobs']
) =>
jobSetupResponses.filter(
jobSetupResponse =>
jobSetupResponse.id === jobId && jobSetupResponse.success && !jobSetupResponse.error
).length > 0;

const hasSuccessfullyStartedDatafeed = (datafeedId: string) => (
datafeedSetupResponses: SetupMlModuleResponsePayload['datafeeds']
) =>
datafeedSetupResponses.filter(
datafeedSetupResponse =>
datafeedSetupResponse.id === datafeedId &&
datafeedSetupResponse.success &&
datafeedSetupResponse.started &&
!datafeedSetupResponse.error
).length > 0;

const getJobStatus = (jobId: string) => (jobSummaries: FetchJobStatusResponsePayload): JobStatus =>
jobSummaries
.filter(jobSummary => jobSummary.id === jobId)
.map(
(jobSummary): JobStatus => {
if (jobSummary.jobState === 'failed') {
return 'failed';
} else if (
jobSummary.jobState === 'closed' &&
jobSummary.datafeedState === 'stopped' &&
jobSummary.fullJob &&
jobSummary.fullJob.finished_time != null
) {
return 'finished';
} else if (
jobSummary.jobState === 'closed' ||
jobSummary.jobState === 'closing' ||
jobSummary.datafeedState === 'stopped'
) {
return 'stopped';
} else if (jobSummary.jobState === 'opening') {
return 'initializing';
} else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') {
return 'started';
}

return 'unknown';
}
)[0] || 'missing';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 { useState, useCallback } from 'react';

interface Props {
setupModule: (startTime?: number | undefined, endTime?: number | undefined) => void;
retrySetup: (startTime?: number | undefined, endTime?: number | undefined) => void;
}

export const useAnalysisSetupState = ({ setupModule, retrySetup }: Props) => {
const [startTime, setStartTime] = useState<number | undefined>(undefined);
const [endTime, setEndTime] = useState<number | undefined>(undefined);
const setup = useCallback(() => {
return setupModule(startTime, endTime);
}, [setupModule, startTime, endTime]);
const retry = useCallback(() => {
return retrySetup(startTime, endTime);
}, [retrySetup, startTime, endTime]);
return {
setup,
retry,
setStartTime,
setEndTime,
startTime,
endTime,
};
};
Loading

0 comments on commit f246c4f

Please sign in to comment.