Skip to content

Commit

Permalink
[Logs UI] Add category data quality warning based on ML job st… (elas…
Browse files Browse the repository at this point in the history
…tic#60551)

This adds warnings to the categories tab when the stats of the underlying ML job indicate a potential problem with the data quality.

closes elastic#60385
  • Loading branch information
weltenwort committed Mar 27, 2020
1 parent b4ee6a8 commit 878ab20
Show file tree
Hide file tree
Showing 22 changed files with 703 additions and 393 deletions.
37 changes: 22 additions & 15 deletions x-pack/plugins/infra/common/log_analysis/log_analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,28 @@ export type JobStatus =
| 'finished'
| 'failed';

export type SetupStatusRequiredReason =
| 'missing' // jobs are missing
| 'reconfiguration' // the configurations don't match the source configurations
| 'update'; // the definitions don't match the module definitions

export type SetupStatus =
| 'initializing' // acquiring job statuses to determine setup status
| 'unknown' // job status could not be acquired (failed request etc)
| 'required' // jobs are missing
| 'requiredForReconfiguration' // the configurations don't match the source configurations
| 'requiredForUpdate' // the definitions don't match the module definitions
| 'pending' // In the process of setting up the module for the first time or retrying, waiting for response
| 'succeeded' // setup succeeded, notifying user
| 'failed' // setup failed, notifying user
| 'hiddenAfterSuccess' // hide the setup screen and we show the results for the first time
| 'skipped' // setup hidden because the module is in a correct state already
| 'skippedButReconfigurable' // setup hidden even though the job configurations are outdated
| 'skippedButUpdatable'; // setup hidden even though the job definitions are outdated
| { type: 'initializing' } // acquiring job statuses to determine setup status
| { type: 'unknown' } // job status could not be acquired (failed request etc)
| {
type: 'required';
reason: SetupStatusRequiredReason;
} // setup required
| { type: 'pending' } // In the process of setting up the module for the first time or retrying, waiting for response
| { type: 'succeeded' } // setup succeeded, notifying user
| {
type: 'failed';
reasons: string[];
} // setup failed, notifying user
| {
type: 'skipped';
newlyCreated?: boolean;
}; // setup is hidden

/**
* Maps a job status to the possibility that results have already been produced
Expand All @@ -43,9 +52,7 @@ export const isHealthyJobStatus = (jobStatus: JobStatus) =>
* produced before this state was reached.
*/
export const isSetupStatusWithResults = (setupStatus: SetupStatus) =>
['skipped', 'hiddenAfterSuccess', 'skippedButReconfigurable', 'skippedButUpdatable'].includes(
setupStatus
);
setupStatus.type === 'skipped';

const KIBANA_SAMPLE_DATA_INDICES = ['kibana_sample_data_logs*'];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,36 @@

import React from 'react';

import { JobStatus, SetupStatus } from '../../../../common/log_analysis';
import { JobConfigurationOutdatedCallout } from './job_configuration_outdated_callout';
import { JobDefinitionOutdatedCallout } from './job_definition_outdated_callout';
import { JobStoppedCallout } from './job_stopped_callout';
import { FirstUseCallout } from '../log_analysis_results';

export const LogAnalysisJobProblemIndicator: React.FC<{
jobStatus: JobStatus;
setupStatus: SetupStatus;
hasOutdatedJobConfigurations: boolean;
hasOutdatedJobDefinitions: boolean;
hasStoppedJobs: boolean;
isFirstUse: boolean;
onRecreateMlJobForReconfiguration: () => void;
onRecreateMlJobForUpdate: () => void;
}> = ({ jobStatus, setupStatus, onRecreateMlJobForReconfiguration, onRecreateMlJobForUpdate }) => {
if (isStopped(jobStatus)) {
return <JobStoppedCallout />;
} else if (isUpdatable(setupStatus)) {
return <JobDefinitionOutdatedCallout onRecreateMlJob={onRecreateMlJobForUpdate} />;
} else if (isReconfigurable(setupStatus)) {
return <JobConfigurationOutdatedCallout onRecreateMlJob={onRecreateMlJobForReconfiguration} />;
}

return null; // no problem to indicate
}> = ({
hasOutdatedJobConfigurations,
hasOutdatedJobDefinitions,
hasStoppedJobs,
isFirstUse,
onRecreateMlJobForReconfiguration,
onRecreateMlJobForUpdate,
}) => {
return (
<>
{hasOutdatedJobDefinitions ? (
<JobDefinitionOutdatedCallout onRecreateMlJob={onRecreateMlJobForUpdate} />
) : null}
{hasOutdatedJobConfigurations ? (
<JobConfigurationOutdatedCallout onRecreateMlJob={onRecreateMlJobForReconfiguration} />
) : null}
{hasStoppedJobs ? <JobStoppedCallout /> : null}
{isFirstUse ? <FirstUseCallout /> : null}
</>
);
};

const isStopped = (jobStatus: JobStatus) => jobStatus === 'stopped';

const isUpdatable = (setupStatus: SetupStatus) => setupStatus === 'skippedButUpdatable';

const isReconfigurable = (setupStatus: SetupStatus) => setupStatus === 'skippedButReconfigurable';

export const jobHasProblem = (jobStatus: JobStatus, setupStatus: SetupStatus) =>
isStopped(jobStatus) || isUpdatable(setupStatus) || isReconfigurable(setupStatus);
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const InitialConfigurationStep: React.FunctionComponent<InitialConfigurat
setValidatedIndices,
validationErrors = [],
}: InitialConfigurationStepProps) => {
const disabled = useMemo(() => !editableFormStatus.includes(setupStatus), [setupStatus]);
const disabled = useMemo(() => !editableFormStatus.includes(setupStatus.type), [setupStatus]);

return (
<>
Expand All @@ -72,12 +72,7 @@ export const InitialConfigurationStep: React.FunctionComponent<InitialConfigurat
);
};

const editableFormStatus = [
'required',
'requiredForReconfiguration',
'requiredForUpdate',
'failed',
];
const editableFormStatus = ['required', 'failed'];

const errorCalloutTitle = i18n.translate(
'xpack.infra.analysisSetup.steps.initialConfigurationStep.errorCalloutTitle',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ export const createProcessStep = (props: ProcessStepProps): EuiContainedStepProp
title: processStepTitle,
children: <ProcessStep {...props} />,
status:
props.setupStatus === 'pending'
props.setupStatus.type === 'pending'
? 'incomplete'
: props.setupStatus === 'failed'
: props.setupStatus.type === 'failed'
? 'danger'
: props.setupStatus === 'succeeded'
: props.setupStatus.type === 'succeeded'
? 'complete'
: undefined,
});
Expand All @@ -55,7 +55,7 @@ export const ProcessStep: React.FunctionComponent<ProcessStepProps> = ({
}) => {
return (
<EuiText size="s">
{setupStatus === 'pending' ? (
{setupStatus.type === 'pending' ? (
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
Expand All @@ -67,7 +67,7 @@ export const ProcessStep: React.FunctionComponent<ProcessStepProps> = ({
/>
</EuiFlexItem>
</EuiFlexGroup>
) : setupStatus === 'failed' ? (
) : setupStatus.type === 'failed' ? (
<>
<FormattedMessage
id="xpack.infra.analysisSetup.steps.setupProcess.failureText"
Expand All @@ -87,7 +87,7 @@ export const ProcessStep: React.FunctionComponent<ProcessStepProps> = ({
/>
</EuiButton>
</>
) : setupStatus === 'succeeded' ? (
) : setupStatus.type === 'succeeded' ? (
<>
<FormattedMessage
id="xpack.infra.analysisSetup.steps.setupProcess.successText"
Expand All @@ -101,7 +101,8 @@ export const ProcessStep: React.FunctionComponent<ProcessStepProps> = ({
/>
</EuiButton>
</>
) : setupStatus === 'requiredForUpdate' || setupStatus === 'requiredForReconfiguration' ? (
) : setupStatus.type === 'required' &&
(setupStatus.reason === 'update' || setupStatus.reason === 'reconfiguration') ? (
<RecreateMLJobsButton isDisabled={!isConfigurationValid} onClick={cleanUpAndSetUp} />
) : (
<CreateMLJobsButton isDisabled={!isConfigurationValid} onClick={setUp} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ const jobStateRT = rt.keyof({
opening: null,
});

const jobCategorizationStatusRT = rt.keyof({
ok: null,
warn: null,
});

const jobModelSizeStatsRT = rt.type({
categorization_status: jobCategorizationStatusRT,
categorized_doc_count: rt.number,
dead_category_count: rt.number,
frequent_category_count: rt.number,
rare_category_count: rt.number,
total_category_count: rt.number,
});

export type JobModelSizeStats = rt.TypeOf<typeof jobModelSizeStatsRT>;

export const jobSummaryRT = rt.intersection([
rt.type({
id: rt.string,
Expand All @@ -65,6 +81,7 @@ export const jobSummaryRT = rt.intersection([
fullJob: rt.partial({
custom_settings: jobCustomSettingsRT,
finished_time: rt.number,
model_size_stats: jobModelSizeStatsRT,
}),
}),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
export * from './log_analysis_capabilities';
export * from './log_analysis_cleanup';
export * from './log_analysis_module';
export * from './log_analysis_module_configuration';
export * from './log_analysis_module_definition';
export * from './log_analysis_module_status';
export * from './log_analysis_module_types';
export * from './log_analysis_setup_state';

export { JobModelSizeStats, JobSummary } from './api/ml_get_jobs_summary_api';
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useMemo } from 'react';

import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { useModuleStatus } from './log_analysis_module_status';
Expand All @@ -17,36 +17,10 @@ export const useLogAnalysisModule = <JobType extends string>({
sourceConfiguration: ModuleSourceConfiguration;
moduleDescriptor: ModuleDescriptor<JobType>;
}) => {
const { spaceId, sourceId, timestampField, indices } = sourceConfiguration;
const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes, {
bucketSpan: moduleDescriptor.bucketSpan,
indexPattern: indices.join(','),
timestampField,
});
const { spaceId, sourceId, timestampField } = sourceConfiguration;
const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes);

const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
dispatchModuleStatus({ type: 'fetchingModuleDefinition' });
return await moduleDescriptor.getModuleDefinition();
},
onResolve: response => {
dispatchModuleStatus({
type: 'fetchedModuleDefinition',
spaceId,
sourceId,
moduleDefinition: response,
});
},
onReject: () => {
dispatchModuleStatus({ type: 'failedFetchingModuleDefinition' });
},
},
[moduleDescriptor.getModuleDefinition, spaceId, sourceId]
);

const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise(
const [, fetchJobStatus] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
Expand All @@ -68,12 +42,6 @@ export const useLogAnalysisModule = <JobType extends string>({
[spaceId, sourceId]
);

const isLoadingModuleStatus = useMemo(
() =>
fetchJobStatusRequest.state === 'pending' || fetchModuleDefinitionRequest.state === 'pending',
[fetchJobStatusRequest.state, fetchModuleDefinitionRequest.state]
);

const [, setUpModule] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
Expand All @@ -83,15 +51,24 @@ export const useLogAnalysisModule = <JobType extends string>({
end: number | undefined
) => {
dispatchModuleStatus({ type: 'startedSetup' });
return await moduleDescriptor.setUpModule(start, end, {
const setupResult = await moduleDescriptor.setUpModule(start, end, {
indices: selectedIndices,
sourceId,
spaceId,
timestampField,
});
const jobSummaries = await moduleDescriptor.getJobSummary(spaceId, sourceId);
return { setupResult, jobSummaries };
},
onResolve: ({ datafeeds, jobs }) => {
dispatchModuleStatus({ type: 'finishedSetup', datafeeds, jobs, spaceId, sourceId });
onResolve: ({ setupResult: { datafeeds, jobs }, jobSummaries }) => {
dispatchModuleStatus({
type: 'finishedSetup',
datafeedSetupResults: datafeeds,
jobSetupResults: jobs,
jobSummaries,
spaceId,
sourceId,
});
},
onReject: () => {
dispatchModuleStatus({ type: 'failedSetup' });
Expand Down Expand Up @@ -146,36 +123,14 @@ export const useLogAnalysisModule = <JobType extends string>({
sourceId,
]);

useEffect(() => {
dispatchModuleStatus({
type: 'updatedSourceConfiguration',
spaceId,
sourceId,
sourceConfiguration: {
timestampField,
indexPattern: indices.join(','),
bucketSpan: moduleDescriptor.bucketSpan,
},
});
}, [
dispatchModuleStatus,
indices,
moduleDescriptor.bucketSpan,
sourceConfiguration,
sourceId,
spaceId,
timestampField,
]);

return {
cleanUpAndSetUpModule,
cleanUpModule,
fetchJobStatus,
fetchModuleDefinition,
isCleaningUp,
isLoadingModuleStatus,
jobIds,
jobStatus: moduleStatus.jobStatus,
jobSummaries: moduleStatus.jobSummaries,
lastSetupErrorMessages: moduleStatus.lastSetupErrorMessages,
moduleDescriptor,
setUpModule,
Expand Down
Loading

0 comments on commit 878ab20

Please sign in to comment.