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] Interpret finished analysis jobs as healthy #45268

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -34,28 +34,33 @@ export const fetchJobStatusRequestPayloadRT = rt.type({

export type FetchJobStatusRequestPayload = rt.TypeOf<typeof fetchJobStatusRequestPayloadRT>;

// TODO: Get this to align with the payload - something is tripping it up somewhere
// export const fetchJobStatusResponsePayloadRT = rt.array(rt.type({
// datafeedId: rt.string,
// datafeedIndices: rt.array(rt.string),
// datafeedState: rt.string,
// description: rt.string,
// earliestTimestampMs: rt.number,
// groups: rt.array(rt.string),
// hasDatafeed: rt.boolean,
// id: rt.string,
// isSingleMetricViewerJob: rt.boolean,
// jobState: rt.string,
// latestResultsTimestampMs: rt.number,
// latestTimestampMs: rt.number,
// memory_status: rt.string,
// nodeName: rt.union([rt.string, rt.undefined]),
// processed_record_count: rt.number,
// fullJob: rt.any,
// auditMessage: rt.any,
// deleting: rt.union([rt.boolean, rt.undefined]),
// }));

export const fetchJobStatusResponsePayloadRT = rt.any;
const datafeedStateRT = rt.keyof({
started: null,
stopped: null,
});

const jobStateRT = rt.keyof({
closed: null,
closing: null,
failed: null,
opened: null,
opening: null,
});

export const jobSummaryRT = rt.intersection([
rt.type({
id: rt.string,
jobState: jobStateRT,
}),
rt.partial({
datafeedIndices: rt.array(rt.string),
datafeedState: datafeedStateRT,
fullJob: rt.partial({
finished_time: rt.number,
}),
}),
]);

export const fetchJobStatusResponsePayloadRT = rt.array(jobSummaryRT);

export type FetchJobStatusResponsePayload = rt.TypeOf<typeof fetchJobStatusResponsePayloadRT>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,23 @@
*/

import createContainer from 'constate-latest';
import { useMemo, useEffect, useState } from 'react';
import { bucketSpan, getJobId } from '../../../../common/log_analysis';
import { useEffect, useMemo, useState } from 'react';

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

// combines and abstracts job and datafeed status
type JobStatus =
| 'unknown'
| 'missing'
| 'inconsistent'
| 'created'
| 'initializing'
| 'stopped'
| 'started'
| 'opening'
| 'opened'
| 'finished'
| 'failed';

interface AllJobStatuses {
[key: string]: JobStatus;
}

const getInitialJobStatuses = (): AllJobStatuses => {
return {
logEntryRate: 'unknown',
};
};

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

const [setupMlModuleRequest, setupMlModule] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async (start, end) => {
setJobStatus(getInitialJobStatuses());
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate': 'initializing',
Copy link
Contributor

Choose a reason for hiding this comment

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

The only part I'm not sure about is this, and it's maybe not a big problem overall. We use jobSummary.jobState === 'opening' to determine a jobStatus state of initializing, my understanding is this would be when everything has been created successfully my ML, but is still opening up. But here we're setting that in the createPromise of setupMlModule when things haven't been created yet. Maybe unknown would be a better state at this point?

Copy link
Member Author

Choose a reason for hiding this comment

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

If we look at the JobStatus as an indicator of the current stage of the (abstract) job/datafeed lifecycle, wouldn't unknown be incorrect? We know the state of the job, which is that it's being created but not running yet. Do you think we need to differentiate that more or that there is a risk in decoupling our abstract job state too much from the technical ML job state?

Copy link
Contributor

Choose a reason for hiding this comment

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

We know the state of the job, which is that it's being created but not running yet.

Okay, put like that it makes sense 👍 No, lets not add another state if it's not needed. Thanks for the explanation.

}));
return await callSetupMlModuleAPI(
start,
end,
Expand All @@ -62,26 +57,25 @@ export const useLogAnalysisJobs = ({
);
},
onResolve: ({ datafeeds, jobs }: SetupMlModuleResponsePayload) => {
const hasSuccessfullyCreatedJobs = jobs.every(job => job.success);
const hasSuccessfullyStartedDatafeeds = datafeeds.every(
datafeed => datafeed.success && datafeed.started
);
const hasAnyErrors =
jobs.some(job => !!job.error) || datafeeds.some(datafeed => !!datafeed.error);

setJobStatus(currentJobStatus => ({
...currentJobStatus,
logEntryRate: hasAnyErrors
? 'failed'
: hasSuccessfullyCreatedJobs
? hasSuccessfullyStartedDatafeeds
'log-entry-rate':
hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, 'log-entry-rate'))(jobs) &&
hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, 'log-entry-rate'))(
datafeeds
)
? 'started'
: 'failed'
: 'failed',
: 'failed',
}));

setHasCompletedSetup(true);
},
onReject: () => {
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate': 'failed',
}));
},
},
[indexPattern, spaceId, sourceId]
);
Expand All @@ -90,20 +84,19 @@ export const useLogAnalysisJobs = ({
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
return callJobsSummaryAPI(spaceId, sourceId);
return await callJobsSummaryAPI(spaceId, sourceId);
},
onResolve: response => {
if (response && response.length) {
const logEntryRate = response.find(
(job: any) => job.id === getJobId(spaceId, sourceId, 'log-entry-rate')
);
setJobStatus({
logEntryRate: logEntryRate ? logEntryRate.jobState : 'unknown',
});
}
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate': getJobStatus(getJobId(spaceId, sourceId, 'log-entry-rate'))(response),
}));
},
onReject: error => {
// TODO: Handle errors
onReject: err => {
setJobStatus(currentJobStatus => ({
...currentJobStatus,
'log-entry-rate': 'unknown',
}));
},
},
[indexPattern, spaceId, sourceId]
Expand All @@ -114,11 +107,7 @@ export const useLogAnalysisJobs = ({
}, []);

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

const isLoadingSetupStatus = useMemo(() => fetchJobStatusRequest.state === 'pending', [
Expand Down Expand Up @@ -147,3 +136,52 @@ export const useLogAnalysisJobs = ({
};

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';