[Logs UI] Show log analysis ML jobs in a list (#71132)

This modifies the ML job setup flyout of the anomalies tab to offer a list of the two available modules. Via the list each of the modules' jobs can be created or re-created.
This commit is contained in:
Felix Stürmer 2020-07-13 23:29:55 +02:00 committed by GitHub
parent 29580bee4e
commit ff7b736cc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 715 additions and 359 deletions

View file

@ -14,18 +14,10 @@ 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 =
| { 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: 'required' } // 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
| {

View file

@ -5,4 +5,5 @@
*/
export * from './log_analysis_job_problem_indicator';
export * from './notices_section';
export * from './recreate_job_button';

View file

@ -11,19 +11,24 @@ import React from 'react';
import { RecreateJobCallout } from './recreate_job_callout';
export const JobConfigurationOutdatedCallout: React.FC<{
moduleName: string;
onRecreateMlJob: () => void;
}> = ({ onRecreateMlJob }) => (
<RecreateJobCallout title={jobConfigurationOutdatedTitle} onRecreateMlJob={onRecreateMlJob}>
}> = ({ moduleName, onRecreateMlJob }) => (
<RecreateJobCallout
title={i18n.translate('xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutTitle', {
defaultMessage: 'The {moduleName} ML job configuration is outdated',
values: {
moduleName,
},
})}
onRecreateMlJob={onRecreateMlJob}
>
<FormattedMessage
id="xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutMessage"
defaultMessage="The ML job was created using a different source configuration. Recreate the job to apply the current configuration. This removes previously detected anomalies."
defaultMessage="The {moduleName} ML job was created using a different source configuration. Recreate the job to apply the current configuration. This removes previously detected anomalies."
values={{
moduleName,
}}
/>
</RecreateJobCallout>
);
const jobConfigurationOutdatedTitle = i18n.translate(
'xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutTitle',
{
defaultMessage: 'ML job configuration outdated',
}
);

View file

@ -11,19 +11,24 @@ import React from 'react';
import { RecreateJobCallout } from './recreate_job_callout';
export const JobDefinitionOutdatedCallout: React.FC<{
moduleName: string;
onRecreateMlJob: () => void;
}> = ({ onRecreateMlJob }) => (
<RecreateJobCallout title={jobDefinitionOutdatedTitle} onRecreateMlJob={onRecreateMlJob}>
}> = ({ moduleName, onRecreateMlJob }) => (
<RecreateJobCallout
title={i18n.translate('xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutTitle', {
defaultMessage: 'The {moduleName} ML job definition is outdated',
values: {
moduleName,
},
})}
onRecreateMlJob={onRecreateMlJob}
>
<FormattedMessage
id="xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutMessage"
defaultMessage="A newer version of the ML job is available. Recreate the job to deploy the newer version. This removes previously detected anomalies."
defaultMessage="A newer version of the {moduleName} ML job is available. Recreate the job to deploy the newer version. This removes previously detected anomalies."
values={{
moduleName,
}}
/>
</RecreateJobCallout>
);
const jobDefinitionOutdatedTitle = i18n.translate(
'xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutTitle',
{
defaultMessage: 'ML job definition outdated',
}
);

View file

@ -16,6 +16,7 @@ export const LogAnalysisJobProblemIndicator: React.FC<{
hasOutdatedJobDefinitions: boolean;
hasStoppedJobs: boolean;
isFirstUse: boolean;
moduleName: string;
onRecreateMlJobForReconfiguration: () => void;
onRecreateMlJobForUpdate: () => void;
}> = ({
@ -23,16 +24,23 @@ export const LogAnalysisJobProblemIndicator: React.FC<{
hasOutdatedJobDefinitions,
hasStoppedJobs,
isFirstUse,
moduleName,
onRecreateMlJobForReconfiguration,
onRecreateMlJobForUpdate,
}) => {
return (
<>
{hasOutdatedJobDefinitions ? (
<JobDefinitionOutdatedCallout onRecreateMlJob={onRecreateMlJobForUpdate} />
<JobDefinitionOutdatedCallout
moduleName={moduleName}
onRecreateMlJob={onRecreateMlJobForUpdate}
/>
) : null}
{hasOutdatedJobConfigurations ? (
<JobConfigurationOutdatedCallout onRecreateMlJob={onRecreateMlJobForReconfiguration} />
<JobConfigurationOutdatedCallout
moduleName={moduleName}
onRecreateMlJob={onRecreateMlJobForReconfiguration}
/>
) : null}
{hasStoppedJobs ? <JobStoppedCallout /> : null}
{isFirstUse ? <FirstUseCallout /> : null}

View file

@ -5,8 +5,8 @@
*/
import React from 'react';
import { LogAnalysisJobProblemIndicator } from '../../../../../components/logging/log_analysis_job_status';
import { QualityWarning } from './quality_warnings';
import { QualityWarning } from '../../../containers/logs/log_analysis/log_analysis_module_types';
import { LogAnalysisJobProblemIndicator } from './log_analysis_job_problem_indicator';
import { CategoryQualityWarnings } from './quality_warning_notices';
export const CategoryJobNoticesSection: React.FC<{
@ -14,6 +14,7 @@ export const CategoryJobNoticesSection: React.FC<{
hasOutdatedJobDefinitions: boolean;
hasStoppedJobs: boolean;
isFirstUse: boolean;
moduleName: string;
onRecreateMlJobForReconfiguration: () => void;
onRecreateMlJobForUpdate: () => void;
qualityWarnings: QualityWarning[];
@ -22,6 +23,7 @@ export const CategoryJobNoticesSection: React.FC<{
hasOutdatedJobDefinitions,
hasStoppedJobs,
isFirstUse,
moduleName,
onRecreateMlJobForReconfiguration,
onRecreateMlJobForUpdate,
qualityWarnings,
@ -32,6 +34,7 @@ export const CategoryJobNoticesSection: React.FC<{
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasStoppedJobs={hasStoppedJobs}
isFirstUse={isFirstUse}
moduleName={moduleName}
onRecreateMlJobForReconfiguration={onRecreateMlJobForReconfiguration}
onRecreateMlJobForUpdate={onRecreateMlJobForUpdate}
/>

View file

@ -8,7 +8,10 @@ import { EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { CategoryQualityWarningReason, QualityWarning } from './quality_warnings';
import type {
CategoryQualityWarningReason,
QualityWarning,
} from '../../../containers/logs/log_analysis/log_analysis_module_types';
export const CategoryQualityWarnings: React.FC<{ qualityWarnings: QualityWarning[] }> = ({
qualityWarnings,

View file

@ -84,7 +84,7 @@ export const InitialConfigurationStep: React.FunctionComponent<InitialConfigurat
);
};
const editableFormStatus = ['required', 'failed'];
const editableFormStatus = ['required', 'failed', 'skipped'];
const errorCalloutTitle = i18n.translate(
'xpack.infra.analysisSetup.steps.initialConfigurationStep.errorCalloutTitle',

View file

@ -0,0 +1,18 @@
/*
* 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 { EuiButton, PropsOf } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
export const ManageJobsButton: React.FunctionComponent<PropsOf<typeof EuiButton>> = (props) => (
<EuiButton {...props}>
<FormattedMessage
id="xpack.infra.logs.analysis.manageMlJobsButtonLabel"
defaultMessage="Manage ML jobs"
/>
</EuiButton>
);

View file

@ -101,11 +101,10 @@ export const ProcessStep: React.FunctionComponent<ProcessStepProps> = ({
/>
</EuiButton>
</>
) : setupStatus.type === 'required' &&
(setupStatus.reason === 'update' || setupStatus.reason === 'reconfiguration') ? (
<RecreateMLJobsButton isDisabled={!isConfigurationValid} onClick={cleanUpAndSetUp} />
) : (
) : setupStatus.type === 'required' ? (
<CreateMLJobsButton isDisabled={!isConfigurationValid} onClick={setUp} />
) : (
<RecreateMLJobsButton isDisabled={!isConfigurationValid} onClick={cleanUpAndSetUp} />
)}
</EuiText>
);

View file

@ -3,3 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export * from './setup_flyout';
export * from './setup_flyout_state';

View file

@ -0,0 +1,87 @@
/*
* 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 { EuiSpacer, EuiSteps, EuiText, EuiTitle } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { useLogEntryCategoriesSetup } from '../../../../containers/logs/log_analysis/modules/log_entry_categories';
import { createInitialConfigurationStep } from '../initial_configuration_step';
import { createProcessStep } from '../process_step';
export const LogEntryCategoriesSetupView: React.FC<{
onClose: () => void;
}> = ({ onClose }) => {
const {
cleanUpAndSetUp,
endTime,
isValidating,
lastSetupErrorMessages,
moduleDescriptor,
setEndTime,
setStartTime,
setValidatedIndices,
setUp,
setupStatus,
startTime,
validatedIndices,
validationErrors,
viewResults,
} = useLogEntryCategoriesSetup();
const viewResultsAndClose = useCallback(() => {
viewResults();
onClose();
}, [viewResults, onClose]);
const steps = useMemo(
() => [
createInitialConfigurationStep({
setStartTime,
setEndTime,
startTime,
endTime,
isValidating,
validatedIndices,
setupStatus,
setValidatedIndices,
validationErrors,
}),
createProcessStep({
cleanUpAndSetUp,
errorMessages: lastSetupErrorMessages,
isConfigurationValid: validationErrors.length <= 0 && !isValidating,
setUp,
setupStatus,
viewResults: viewResultsAndClose,
}),
],
[
cleanUpAndSetUp,
endTime,
isValidating,
lastSetupErrorMessages,
setEndTime,
setStartTime,
setUp,
setValidatedIndices,
setupStatus,
startTime,
validatedIndices,
validationErrors,
viewResultsAndClose,
]
);
return (
<>
<EuiTitle size="s">
<h3>{moduleDescriptor.moduleName} </h3>
</EuiTitle>
<EuiText size="s">{moduleDescriptor.moduleDescription}</EuiText>
<EuiSpacer />
<EuiSteps steps={steps} />
</>
);
};

View file

@ -5,37 +5,20 @@
*/
import React, { useMemo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiTitle,
EuiText,
EuiSpacer,
EuiSteps,
} from '@elastic/eui';
import { EuiTitle, EuiText, EuiSpacer, EuiSteps } from '@elastic/eui';
import { createInitialConfigurationStep } from '../initial_configuration_step';
import { createProcessStep } from '../process_step';
import { useLogEntryRateSetup } from '../../../../containers/logs/log_analysis/modules/log_entry_rate';
import {
createInitialConfigurationStep,
createProcessStep,
} from '../../../components/logging/log_analysis_setup';
import { useLogEntryRateSetup } from './use_log_entry_rate_setup';
interface LogEntryRateSetupFlyoutProps {
isOpen: boolean;
export const LogEntryRateSetupView: React.FC<{
onClose: () => void;
}
export const LogEntryRateSetupFlyout: React.FC<LogEntryRateSetupFlyoutProps> = ({
isOpen,
onClose,
}) => {
}> = ({ onClose }) => {
const {
cleanUpAndSetUp,
endTime,
isValidating,
lastSetupErrorMessages,
moduleDescriptor,
setEndTime,
setStartTime,
setValidatedIndices,
@ -91,39 +74,14 @@ export const LogEntryRateSetupFlyout: React.FC<LogEntryRateSetupFlyoutProps> = (
]
);
if (!isOpen) {
return null;
}
return (
<EuiFlyout onClose={onClose}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.infra.logs.setupFlyout.setupFlyoutTitle"
defaultMessage="Anomaly detection with Machine Learning"
/>
</h3>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.infra.logs.setupFlyout.logRateTitle"
defaultMessage="Log rate"
/>
</h3>
</EuiTitle>
<EuiText size="s">
<FormattedMessage
id="xpack.infra.logs.setupFlyout.logRateDescription"
defaultMessage="Use Machine Learning to automatically detect anomalous log rate counts."
/>
</EuiText>
<EuiSpacer />
<EuiSteps steps={steps} />
</EuiFlyoutBody>
</EuiFlyout>
<>
<EuiTitle size="s">
<h3>{moduleDescriptor.moduleName} </h3>
</EuiTitle>
<EuiText size="s">{moduleDescriptor.moduleDescription}</EuiText>
<EuiSpacer />
<EuiSteps steps={steps} />
</>
);
};

View file

@ -0,0 +1,55 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback } from 'react';
import {
logEntryCategoriesModule,
useLogEntryCategoriesModuleContext,
} from '../../../../containers/logs/log_analysis/modules/log_entry_categories';
import {
logEntryRateModule,
useLogEntryRateModuleContext,
} from '../../../../containers/logs/log_analysis/modules/log_entry_rate';
import { LogAnalysisModuleListCard } from './module_list_card';
import type { ModuleId } from './setup_flyout_state';
export const LogAnalysisModuleList: React.FC<{
onViewModuleSetup: (module: ModuleId) => void;
}> = ({ onViewModuleSetup }) => {
const { setupStatus: logEntryRateSetupStatus } = useLogEntryRateModuleContext();
const { setupStatus: logEntryCategoriesSetupStatus } = useLogEntryCategoriesModuleContext();
const viewLogEntryRateSetupFlyout = useCallback(() => {
onViewModuleSetup('logs_ui_analysis');
}, [onViewModuleSetup]);
const viewLogEntryCategoriesSetupFlyout = useCallback(() => {
onViewModuleSetup('logs_ui_categories');
}, [onViewModuleSetup]);
return (
<>
<EuiFlexGroup>
<EuiFlexItem>
<LogAnalysisModuleListCard
moduleDescription={logEntryRateModule.moduleDescription}
moduleName={logEntryRateModule.moduleName}
moduleStatus={logEntryRateSetupStatus}
onViewSetup={viewLogEntryRateSetupFlyout}
/>
</EuiFlexItem>
<EuiFlexItem>
<LogAnalysisModuleListCard
moduleDescription={logEntryCategoriesModule.moduleDescription}
moduleName={logEntryCategoriesModule.moduleName}
moduleStatus={logEntryCategoriesSetupStatus}
onViewSetup={viewLogEntryCategoriesSetupFlyout}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -0,0 +1,46 @@
/*
* 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 { EuiCard, EuiIcon } from '@elastic/eui';
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { RecreateJobButton } from '../../log_analysis_job_status';
import { SetupStatus } from '../../../../../common/log_analysis';
export const LogAnalysisModuleListCard: React.FC<{
moduleDescription: string;
moduleName: string;
moduleStatus: SetupStatus;
onViewSetup: () => void;
}> = ({ moduleDescription, moduleName, moduleStatus, onViewSetup }) => {
const icon =
moduleStatus.type === 'required' ? (
<EuiIcon size="xxl" type="machineLearningApp" />
) : (
<EuiIcon color="secondary" size="xxl" type="check" />
);
const footerContent =
moduleStatus.type === 'required' ? (
<EuiButton onClick={onViewSetup}>
<FormattedMessage
id="xpack.infra.logs.analysis.enableAnomalyDetectionButtonLabel"
defaultMessage="Enable anomaly detection"
/>
</EuiButton>
) : (
<RecreateJobButton onClick={onViewSetup} />
);
return (
<EuiCard
description={moduleDescription}
footer={<div>{footerContent}</div>}
icon={icon}
title={moduleName}
/>
);
};

View file

@ -0,0 +1,80 @@
/*
* 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 {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { LogEntryRateSetupView } from './log_entry_rate_setup_view';
import { LogEntryCategoriesSetupView } from './log_entry_categories_setup_view';
import { LogAnalysisModuleList } from './module_list';
import { useLogAnalysisSetupFlyoutStateContext } from './setup_flyout_state';
const FLYOUT_HEADING_ID = 'logAnalysisSetupFlyoutHeading';
export const LogAnalysisSetupFlyout: React.FC = () => {
const {
closeFlyout,
flyoutView,
showModuleList,
showModuleSetup,
} = useLogAnalysisSetupFlyoutStateContext();
if (flyoutView.view === 'hidden') {
return null;
}
return (
<EuiFlyout aria-labelledby={FLYOUT_HEADING_ID} maxWidth={800} onClose={closeFlyout}>
<EuiFlyoutHeader hasBorder>
<EuiTitle>
<h2 id={FLYOUT_HEADING_ID}>
<FormattedMessage
id="xpack.infra.logs.analysis.setupFlyoutTitle"
defaultMessage="Anomaly detection with Machine Learning"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{flyoutView.view === 'moduleList' ? (
<LogAnalysisModuleList onViewModuleSetup={showModuleSetup} />
) : flyoutView.view === 'moduleSetup' && flyoutView.module === 'logs_ui_analysis' ? (
<LogAnalysisSetupFlyoutSubPage onViewModuleList={showModuleList}>
<LogEntryRateSetupView onClose={closeFlyout} />
</LogAnalysisSetupFlyoutSubPage>
) : flyoutView.view === 'moduleSetup' && flyoutView.module === 'logs_ui_categories' ? (
<LogAnalysisSetupFlyoutSubPage onViewModuleList={showModuleList}>
<LogEntryCategoriesSetupView onClose={closeFlyout} />
</LogAnalysisSetupFlyoutSubPage>
) : null}
</EuiFlyoutBody>
</EuiFlyout>
);
};
const LogAnalysisSetupFlyoutSubPage: React.FC<{
onViewModuleList: () => void;
}> = ({ children, onViewModuleList }) => (
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiButtonEmpty flush="left" iconSide="left" iconType="arrowLeft" onClick={onViewModuleList}>
<FormattedMessage
id="xpack.infra.logs.analysis.setupFlyoutGotoListButtonLabel"
defaultMessage="All Machine Learning jobs"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>{children}</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,45 @@
/*
* 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 createContainer from 'constate';
import { useState, useCallback } from 'react';
export type ModuleId = 'logs_ui_analysis' | 'logs_ui_categories';
type FlyoutView =
| { view: 'hidden' }
| { view: 'moduleList' }
| { view: 'moduleSetup'; module: ModuleId };
export const useLogAnalysisSetupFlyoutState = ({
initialFlyoutView = { view: 'hidden' },
}: {
initialFlyoutView?: FlyoutView;
}) => {
const [flyoutView, setFlyoutView] = useState<FlyoutView>(initialFlyoutView);
const closeFlyout = useCallback(() => setFlyoutView({ view: 'hidden' }), []);
const showModuleList = useCallback(() => setFlyoutView({ view: 'moduleList' }), []);
const showModuleSetup = useCallback(
(module: ModuleId) => {
setFlyoutView({ view: 'moduleSetup', module });
},
[setFlyoutView]
);
return {
closeFlyout,
flyoutView,
setFlyoutView,
showModuleList,
showModuleSetup,
};
};
export const [
LogAnalysisSetupFlyoutStateProvider,
useLogAnalysisSetupFlyoutStateContext,
] = createContainer(useLogAnalysisSetupFlyoutState);

View file

@ -111,14 +111,6 @@ export const useLogAnalysisModule = <JobType extends string>({
[cleanUpModule, dispatchModuleStatus, setUpModule]
);
const viewSetupForReconfiguration = useCallback(() => {
dispatchModuleStatus({ type: 'requestedJobConfigurationUpdate' });
}, [dispatchModuleStatus]);
const viewSetupForUpdate = useCallback(() => {
dispatchModuleStatus({ type: 'requestedJobDefinitionUpdate' });
}, [dispatchModuleStatus]);
const viewResults = useCallback(() => {
dispatchModuleStatus({ type: 'viewedResults' });
}, [dispatchModuleStatus]);
@ -143,7 +135,5 @@ export const useLogAnalysisModule = <JobType extends string>({
setupStatus: moduleStatus.setupStatus,
sourceConfiguration,
viewResults,
viewSetupForReconfiguration,
viewSetupForUpdate,
};
};

View file

@ -43,8 +43,6 @@ type StatusReducerAction =
payload: FetchJobStatusResponsePayload;
}
| { type: 'failedFetchingJobStatuses' }
| { type: 'requestedJobConfigurationUpdate' }
| { type: 'requestedJobDefinitionUpdate' }
| { type: 'viewedResults' };
const createInitialState = <JobType extends string>({
@ -173,18 +171,6 @@ const createStatusReducer = <JobType extends string>(jobTypes: JobType[]) => (
),
};
}
case 'requestedJobConfigurationUpdate': {
return {
...state,
setupStatus: { type: 'required', reason: 'reconfiguration' },
};
}
case 'requestedJobDefinitionUpdate': {
return {
...state,
setupStatus: { type: 'required', reason: 'update' },
};
}
case 'viewedResults': {
return {
...state,
@ -251,7 +237,7 @@ const getSetupStatus = <JobType extends string>(everyJobStatus: Record<JobType,
): SetupStatus =>
Object.entries<JobStatus>(everyJobStatus).reduce<SetupStatus>((setupStatus, [, jobStatus]) => {
if (jobStatus === 'missing') {
return { type: 'required', reason: 'missing' };
return { type: 'required' };
} else if (setupStatus.type === 'required' || setupStatus.type === 'succeeded') {
return setupStatus;
} else if (setupStatus.type === 'skipped' || isJobStatusWithResults(jobStatus)) {

View file

@ -4,18 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
ValidateLogEntryDatasetsResponsePayload,
ValidationIndicesResponsePayload,
} from '../../../../common/http_api/log_analysis';
import { DatasetFilter } from '../../../../common/log_analysis';
import { DeleteJobsResponsePayload } from './api/ml_cleanup';
import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api';
import { GetMlModuleResponsePayload } from './api/ml_get_module';
import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api';
import {
ValidationIndicesResponsePayload,
ValidateLogEntryDatasetsResponsePayload,
} from '../../../../common/http_api/log_analysis';
import { DatasetFilter } from '../../../../common/log_analysis';
export { JobModelSizeStats, JobSummary } from './api/ml_get_jobs_summary_api';
export interface ModuleDescriptor<JobType extends string> {
moduleId: string;
moduleName: string;
moduleDescription: string;
jobTypes: JobType[];
bucketSpan: number;
getJobIds: (spaceId: string, sourceId: string) => Record<JobType, string>;
@ -46,3 +50,43 @@ export interface ModuleSourceConfiguration {
spaceId: string;
timestampField: string;
}
interface ManyCategoriesWarningReason {
type: 'manyCategories';
categoriesDocumentRatio: number;
}
interface ManyDeadCategoriesWarningReason {
type: 'manyDeadCategories';
deadCategoriesRatio: number;
}
interface ManyRareCategoriesWarningReason {
type: 'manyRareCategories';
rareCategoriesRatio: number;
}
interface NoFrequentCategoriesWarningReason {
type: 'noFrequentCategories';
}
interface SingleCategoryWarningReason {
type: 'singleCategory';
}
export type CategoryQualityWarningReason =
| ManyCategoriesWarningReason
| ManyDeadCategoriesWarningReason
| ManyRareCategoriesWarningReason
| NoFrequentCategoriesWarningReason
| SingleCategoryWarningReason;
export type CategoryQualityWarningReasonType = CategoryQualityWarningReason['type'];
export interface CategoryQualityWarning {
type: 'categoryQualityWarning';
jobId: string;
reasons: CategoryQualityWarningReason[];
}
export type QualityWarning = CategoryQualityWarning;

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export * from './module_descriptor';
export * from './use_log_entry_categories_module';
export * from './use_log_entry_categories_quality';
export * from './use_log_entry_categories_setup';

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import {
bucketSpan,
categoriesMessageField,
@ -12,19 +13,25 @@ import {
LogEntryCategoriesJobType,
logEntryCategoriesJobTypes,
partitionField,
} from '../../../../common/log_analysis';
import {
cleanUpJobsAndDatafeeds,
ModuleDescriptor,
ModuleSourceConfiguration,
} from '../../../containers/logs/log_analysis';
import { callJobsSummaryAPI } from '../../../containers/logs/log_analysis/api/ml_get_jobs_summary_api';
import { callGetMlModuleAPI } from '../../../containers/logs/log_analysis/api/ml_get_module';
import { callSetupMlModuleAPI } from '../../../containers/logs/log_analysis/api/ml_setup_module_api';
import { callValidateDatasetsAPI } from '../../../containers/logs/log_analysis/api/validate_datasets';
import { callValidateIndicesAPI } from '../../../containers/logs/log_analysis/api/validate_indices';
} from '../../../../../../common/log_analysis';
import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api';
import { callGetMlModuleAPI } from '../../api/ml_get_module';
import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api';
import { callValidateDatasetsAPI } from '../../api/validate_datasets';
import { callValidateIndicesAPI } from '../../api/validate_indices';
import { cleanUpJobsAndDatafeeds } from '../../log_analysis_cleanup';
import { ModuleDescriptor, ModuleSourceConfiguration } from '../../log_analysis_module_types';
const moduleId = 'logs_ui_categories';
const moduleName = i18n.translate('xpack.infra.logs.analysis.logEntryCategoriesModuleName', {
defaultMessage: 'Categorization',
});
const moduleDescription = i18n.translate(
'xpack.infra.logs.analysis.logEntryCategoriesModuleDescription',
{
defaultMessage: 'Use Machine Learning to automatically categorize log messages.',
}
);
const getJobIds = (spaceId: string, sourceId: string) =>
logEntryCategoriesJobTypes.reduce(
@ -138,6 +145,8 @@ const validateSetupDatasets = async (
export const logEntryCategoriesModule: ModuleDescriptor<LogEntryCategoriesJobType> = {
moduleId,
moduleName,
moduleDescription,
jobTypes: logEntryCategoriesJobTypes,
bucketSpan,
getJobIds,

View file

@ -6,12 +6,10 @@
import createContainer from 'constate';
import { useMemo } from 'react';
import {
ModuleSourceConfiguration,
useLogAnalysisModule,
useLogAnalysisModuleConfiguration,
useLogAnalysisModuleDefinition,
} from '../../../containers/logs/log_analysis';
import { useLogAnalysisModule } from '../../log_analysis_module';
import { useLogAnalysisModuleConfiguration } from '../../log_analysis_module_configuration';
import { useLogAnalysisModuleDefinition } from '../../log_analysis_module_definition';
import { ModuleSourceConfiguration } from '../../log_analysis_module_types';
import { logEntryCategoriesModule } from './module_descriptor';
import { useLogEntryCategoriesQuality } from './use_log_entry_categories_quality';

View file

@ -5,9 +5,12 @@
*/
import { useMemo } from 'react';
import { JobModelSizeStats, JobSummary } from '../../../containers/logs/log_analysis';
import { QualityWarning, CategoryQualityWarningReason } from './sections/notices/quality_warnings';
import {
JobModelSizeStats,
JobSummary,
QualityWarning,
CategoryQualityWarningReason,
} from '../../log_analysis_module_types';
export const useLogEntryCategoriesQuality = ({ jobSummaries }: { jobSummaries: JobSummary[] }) => {
const categoryQualityWarnings: QualityWarning[] = useMemo(

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useAnalysisSetupState } from '../../../containers/logs/log_analysis';
import { useAnalysisSetupState } from '../../log_analysis_setup_state';
import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module';
export const useLogEntryCategoriesSetup = () => {
@ -41,6 +41,7 @@ export const useLogEntryCategoriesSetup = () => {
endTime,
isValidating,
lastSetupErrorMessages,
moduleDescriptor,
setEndTime,
setStartTime,
setValidatedIndices,

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './module_descriptor';
export * from './use_log_entry_rate_module';
export * from './use_log_entry_rate_setup';

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import {
bucketSpan,
DatasetFilter,
@ -11,19 +12,25 @@ import {
LogEntryRateJobType,
logEntryRateJobTypes,
partitionField,
} from '../../../../common/log_analysis';
import {
cleanUpJobsAndDatafeeds,
ModuleDescriptor,
ModuleSourceConfiguration,
} from '../../../containers/logs/log_analysis';
import { callJobsSummaryAPI } from '../../../containers/logs/log_analysis/api/ml_get_jobs_summary_api';
import { callGetMlModuleAPI } from '../../../containers/logs/log_analysis/api/ml_get_module';
import { callSetupMlModuleAPI } from '../../../containers/logs/log_analysis/api/ml_setup_module_api';
import { callValidateDatasetsAPI } from '../../../containers/logs/log_analysis/api/validate_datasets';
import { callValidateIndicesAPI } from '../../../containers/logs/log_analysis/api/validate_indices';
} from '../../../../../../common/log_analysis';
import { ModuleDescriptor, ModuleSourceConfiguration } from '../../log_analysis_module_types';
import { cleanUpJobsAndDatafeeds } from '../../log_analysis_cleanup';
import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api';
import { callGetMlModuleAPI } from '../../api/ml_get_module';
import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api';
import { callValidateDatasetsAPI } from '../../api/validate_datasets';
import { callValidateIndicesAPI } from '../../api/validate_indices';
const moduleId = 'logs_ui_analysis';
const moduleName = i18n.translate('xpack.infra.logs.analysis.logEntryRateModuleName', {
defaultMessage: 'Log rate',
});
const moduleDescription = i18n.translate(
'xpack.infra.logs.analysis.logEntryRateModuleDescription',
{
defaultMessage: 'Use Machine Learning to automatically detect anomalous log entry rates.',
}
);
const getJobIds = (spaceId: string, sourceId: string) =>
logEntryRateJobTypes.reduce(
@ -126,6 +133,8 @@ const validateSetupDatasets = async (
export const logEntryRateModule: ModuleDescriptor<LogEntryRateJobType> = {
moduleId,
moduleName,
moduleDescription,
jobTypes: logEntryRateJobTypes,
bucketSpan,
getJobIds,

View file

@ -6,12 +6,10 @@
import createContainer from 'constate';
import { useMemo } from 'react';
import {
ModuleSourceConfiguration,
useLogAnalysisModule,
useLogAnalysisModuleConfiguration,
useLogAnalysisModuleDefinition,
} from '../../../containers/logs/log_analysis';
import { ModuleSourceConfiguration } from '../../log_analysis_module_types';
import { useLogAnalysisModule } from '../../log_analysis_module';
import { useLogAnalysisModuleConfiguration } from '../../log_analysis_module_configuration';
import { useLogAnalysisModuleDefinition } from '../../log_analysis_module_definition';
import { logEntryRateModule } from './module_descriptor';
export const useLogEntryRateModule = ({

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useAnalysisSetupState } from '../../../containers/logs/log_analysis';
import createContainer from 'constate';
import { useAnalysisSetupState } from '../../log_analysis_setup_state';
import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
export const useLogEntryRateSetup = () => {
@ -41,6 +42,7 @@ export const useLogEntryRateSetup = () => {
endTime,
isValidating,
lastSetupErrorMessages,
moduleDescriptor,
setEndTime,
setStartTime,
setValidatedIndices,
@ -52,3 +54,7 @@ export const useLogEntryRateSetup = () => {
viewResults,
};
};
export const [LogEntryRateSetupProvider, useLogEntryRateSetupContext] = createContainer(
useLogEntryRateSetup
);

View file

@ -5,7 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState, useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
@ -17,10 +17,10 @@ import {
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogEntryCategoriesResultsContent } from './page_results_content';
import { LogEntryCategoriesSetupContent } from './page_setup_content';
import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module';
import { LogEntryCategoriesSetupFlyout } from './setup_flyout';
export const LogEntryCategoriesPageContent = () => {
@ -50,13 +50,6 @@ export const LogEntryCategoriesPageContent = () => {
}
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
// Open flyout if there are no ML jobs
useEffect(() => {
if (setupStatus.type === 'required' && setupStatus.reason === 'missing') {
openFlyout();
}
}, [setupStatus, openFlyout]);
if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (hasFailedLoadingSource) {

View file

@ -5,10 +5,9 @@
*/
import React from 'react';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id';
import { LogEntryCategoriesModuleProvider } from './use_log_entry_categories_module';
export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => {
const { sourceId, sourceConfiguration } = useLogSourceContext();

View file

@ -12,17 +12,17 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { euiStyled, useTrackPageview } from '../../../../../observability/public';
import { TimeRange } from '../../../../common/http_api/shared/time_range';
import { CategoryJobNoticesSection } from '../../../components/logging/log_analysis_job_status';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
import { useInterval } from '../../../hooks/use_interval';
import { CategoryJobNoticesSection } from './sections/notices/notices_section';
import { PageViewLogInContext } from '../stream/page_view_log_in_context';
import { TopCategoriesSection } from './sections/top_categories';
import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module';
import { useLogEntryCategoriesResults } from './use_log_entry_categories_results';
import {
StringTimeRange,
useLogEntryCategoriesResultsUrlState,
} from './use_log_entry_categories_results_url_state';
import { PageViewLogInContext } from '../stream/page_view_log_in_context';
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
const JOB_STATUS_POLLING_INTERVAL = 30000;
@ -39,9 +39,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
const {
fetchJobStatus,
fetchModuleDefinition,
moduleDescriptor,
setupStatus,
viewSetupForReconfiguration,
viewSetupForUpdate,
hasOutdatedJobConfigurations,
hasOutdatedJobDefinitions,
hasStoppedJobs,
@ -131,16 +130,6 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
[setAutoRefresh]
);
const viewSetupFlyoutForReconfiguration = useCallback(() => {
viewSetupForReconfiguration();
onOpenSetup();
}, [onOpenSetup, viewSetupForReconfiguration]);
const viewSetupFlyoutForUpdate = useCallback(() => {
viewSetupForUpdate();
onOpenSetup();
}, [onOpenSetup, viewSetupForUpdate]);
const hasResults = useMemo(() => topLogEntryCategories.length > 0, [
topLogEntryCategories.length,
]);
@ -210,8 +199,9 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasStoppedJobs={hasStoppedJobs}
isFirstUse={isFirstUse}
onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration}
onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate}
moduleName={moduleDescriptor.moduleName}
onRecreateMlJobForReconfiguration={onOpenSetup}
onRecreateMlJobForUpdate={onOpenSetup}
qualityWarnings={categoryQualityWarnings}
/>
</EuiFlexItem>
@ -223,7 +213,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
isLoadingTopCategories={isLoadingTopLogEntryCategories}
jobId={jobIds['log-entry-categories-count']}
onChangeDatasetSelection={setCategoryQueryDatasets}
onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration}
onRequestRecreateMlJob={onOpenSetup}
selectedDatasets={categoryQueryDatasets}
sourceId={sourceId}
timeRange={categoryQueryTimeRange.timeRange}

View file

@ -1,45 +0,0 @@
/*
* 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.
*/
interface ManyCategoriesWarningReason {
type: 'manyCategories';
categoriesDocumentRatio: number;
}
interface ManyDeadCategoriesWarningReason {
type: 'manyDeadCategories';
deadCategoriesRatio: number;
}
interface ManyRareCategoriesWarningReason {
type: 'manyRareCategories';
rareCategoriesRatio: number;
}
interface NoFrequentCategoriesWarningReason {
type: 'noFrequentCategories';
}
interface SingleCategoryWarningReason {
type: 'singleCategory';
}
export type CategoryQualityWarningReason =
| ManyCategoriesWarningReason
| ManyDeadCategoriesWarningReason
| ManyRareCategoriesWarningReason
| NoFrequentCategoriesWarningReason
| SingleCategoryWarningReason;
export type CategoryQualityWarningReasonType = CategoryQualityWarningReason['type'];
export interface CategoryQualityWarning {
type: 'categoryQualityWarning';
jobId: string;
reasons: CategoryQualityWarningReason[];
}
export type QualityWarning = CategoryQualityWarning;

View file

@ -4,23 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useMemo, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiTitle,
EuiText,
EuiFlyoutHeader,
EuiSpacer,
EuiSteps,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useCallback, useMemo } from 'react';
import {
createInitialConfigurationStep,
createProcessStep,
} from '../../../components/logging/log_analysis_setup';
import { useLogEntryCategoriesSetup } from './use_log_entry_categories_setup';
import { useLogEntryCategoriesSetup } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
interface LogEntryCategoriesSetupFlyoutProps {
isOpen: boolean;

View file

@ -5,7 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState, useCallback } from 'react';
import React, { memo, useEffect, useCallback } from 'react';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
@ -14,16 +14,23 @@ import {
MissingSetupPrivilegesPrompt,
SubscriptionSplashContent,
} from '../../../components/logging/log_analysis_setup';
import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogEntryRateResultsContent } from './page_results_content';
import { LogEntryRateSetupContent } from './page_setup_content';
import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
import { LogEntryRateSetupFlyout } from './setup_flyout';
import { useInterval } from '../../../hooks/use_interval';
export const LogEntryRatePageContent = () => {
const JOB_STATUS_POLLING_INTERVAL = 30000;
export const LogEntryRatePageContent = memo(() => {
const {
hasFailedLoadingSource,
isLoading,
@ -38,24 +45,52 @@ export const LogEntryRatePageContent = () => {
hasLogAnalysisSetupCapabilities,
} = useLogAnalysisCapabilitiesContext();
const { fetchJobStatus, setupStatus, jobStatus } = useLogEntryRateModuleContext();
const {
fetchJobStatus: fetchLogEntryCategoriesJobStatus,
fetchModuleDefinition: fetchLogEntryCategoriesModuleDefinition,
jobStatus: logEntryCategoriesJobStatus,
setupStatus: logEntryCategoriesSetupStatus,
} = useLogEntryCategoriesModuleContext();
const {
fetchJobStatus: fetchLogEntryRateJobStatus,
fetchModuleDefinition: fetchLogEntryRateModuleDefinition,
jobStatus: logEntryRateJobStatus,
setupStatus: logEntryRateSetupStatus,
} = useLogEntryRateModuleContext();
const [isFlyoutOpen, setIsFlyoutOpen] = useState<boolean>(false);
const openFlyout = useCallback(() => setIsFlyoutOpen(true), []);
const closeFlyout = useCallback(() => setIsFlyoutOpen(false), []);
const { showModuleList } = useLogAnalysisSetupFlyoutStateContext();
const fetchAllJobStatuses = useCallback(
() => Promise.all([fetchLogEntryCategoriesJobStatus(), fetchLogEntryRateJobStatus()]),
[fetchLogEntryCategoriesJobStatus, fetchLogEntryRateJobStatus]
);
useEffect(() => {
if (hasLogAnalysisReadCapabilities) {
fetchJobStatus();
fetchAllJobStatuses();
}
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
}, [fetchAllJobStatuses, hasLogAnalysisReadCapabilities]);
// Open flyout if there are no ML jobs
useEffect(() => {
if (setupStatus.type === 'required' && setupStatus.reason === 'missing') {
openFlyout();
if (hasLogAnalysisReadCapabilities) {
fetchLogEntryCategoriesModuleDefinition();
}
}, [setupStatus, openFlyout]);
}, [fetchLogEntryCategoriesModuleDefinition, hasLogAnalysisReadCapabilities]);
useEffect(() => {
if (hasLogAnalysisReadCapabilities) {
fetchLogEntryRateModuleDefinition();
}
}, [fetchLogEntryRateModuleDefinition, hasLogAnalysisReadCapabilities]);
useInterval(() => {
if (logEntryCategoriesSetupStatus.type !== 'pending' && hasLogAnalysisReadCapabilities) {
fetchLogEntryCategoriesJobStatus();
}
if (logEntryRateSetupStatus.type !== 'pending' && hasLogAnalysisReadCapabilities) {
fetchLogEntryRateJobStatus();
}
}, JOB_STATUS_POLLING_INTERVAL);
if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
@ -65,7 +100,10 @@ export const LogEntryRatePageContent = () => {
return <SubscriptionSplashContent />;
} else if (!hasLogAnalysisReadCapabilities) {
return <MissingResultsPrivilegesPrompt />;
} else if (setupStatus.type === 'initializing') {
} else if (
logEntryCategoriesSetupStatus.type === 'initializing' ||
logEntryRateSetupStatus.type === 'initializing'
) {
return (
<LoadingPage
message={i18n.translate('xpack.infra.logs.analysisPage.loadingMessage', {
@ -73,13 +111,19 @@ export const LogEntryRatePageContent = () => {
})}
/>
);
} else if (setupStatus.type === 'unknown') {
return <LogAnalysisSetupStatusUnknownPrompt retry={fetchJobStatus} />;
} else if (isJobStatusWithResults(jobStatus['log-entry-rate'])) {
} else if (
logEntryCategoriesSetupStatus.type === 'unknown' ||
logEntryRateSetupStatus.type === 'unknown'
) {
return <LogAnalysisSetupStatusUnknownPrompt retry={fetchAllJobStatuses} />;
} else if (
isJobStatusWithResults(logEntryCategoriesJobStatus['log-entry-categories-count']) ||
isJobStatusWithResults(logEntryRateJobStatus['log-entry-rate'])
) {
return (
<>
<LogEntryRateResultsContent onOpenSetup={openFlyout} />
<LogEntryRateSetupFlyout isOpen={isFlyoutOpen} onClose={closeFlyout} />
<LogEntryRateResultsContent />
<LogAnalysisSetupFlyout />
</>
);
} else if (!hasLogAnalysisSetupCapabilities) {
@ -87,9 +131,9 @@ export const LogEntryRatePageContent = () => {
} else {
return (
<>
<LogEntryRateSetupContent onOpenSetup={openFlyout} />
<LogEntryRateSetupFlyout isOpen={isFlyoutOpen} onClose={closeFlyout} />
<LogEntryRateSetupContent onOpenSetup={showModuleList} />
<LogAnalysisSetupFlyout />
</>
);
}
};
});

View file

@ -5,10 +5,11 @@
*/
import React from 'react';
import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id';
import { LogEntryRateModuleProvider } from './use_log_entry_rate_module';
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
const { sourceId, sourceConfiguration } = useLogSourceContext();
@ -21,7 +22,14 @@ export const LogEntryRatePageProviders: React.FunctionComponent = ({ children })
spaceId={spaceId}
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
>
{children}
<LogEntryCategoriesModuleProvider
indexPattern={sourceConfiguration?.configuration.logAlias ?? ''}
sourceId={sourceId}
spaceId={spaceId}
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
>
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider>
</LogEntryCategoriesModuleProvider>
</LogEntryRateModuleProvider>
);
};

View file

@ -11,19 +11,23 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { euiStyled, useTrackPageview } from '../../../../../observability/public';
import { TimeRange } from '../../../../common/http_api/shared/time_range';
import { bucketSpan } from '../../../../common/log_analysis';
import { LogAnalysisJobProblemIndicator } from '../../../components/logging/log_analysis_job_status';
import {
CategoryJobNoticesSection,
LogAnalysisJobProblemIndicator,
} from '../../../components/logging/log_analysis_job_status';
import { useLogAnalysisSetupFlyoutStateContext } from '../../../components/logging/log_analysis_setup/setup_flyout';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useInterval } from '../../../hooks/use_interval';
import { AnomaliesResults } from './sections/anomalies';
import { useLogEntryRateModuleContext } from './use_log_entry_rate_module';
import { useLogEntryRateResults } from './use_log_entry_rate_results';
import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results';
import { useLogEntryRateResults } from './use_log_entry_rate_results';
import {
StringTimeRange,
useLogAnalysisResultsUrlState,
} from './use_log_entry_rate_results_url_state';
const JOB_STATUS_POLLING_INTERVAL = 30000;
export const SORT_DEFAULTS = {
direction: 'desc' as const,
field: 'anomalyScore' as const,
@ -33,28 +37,29 @@ export const PAGINATION_DEFAULTS = {
pageSize: 25,
};
interface LogEntryRateResultsContentProps {
onOpenSetup: () => void;
}
export const LogEntryRateResultsContent: React.FunctionComponent<LogEntryRateResultsContentProps> = ({
onOpenSetup,
}) => {
export const LogEntryRateResultsContent: React.FunctionComponent = () => {
useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' });
useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 });
const { sourceId } = useLogSourceContext();
const {
fetchJobStatus,
fetchModuleDefinition,
setupStatus,
viewSetupForReconfiguration,
viewSetupForUpdate,
hasOutdatedJobConfigurations,
hasOutdatedJobDefinitions,
hasStoppedJobs,
sourceConfiguration: { sourceId },
hasOutdatedJobConfigurations: hasOutdatedLogEntryRateJobConfigurations,
hasOutdatedJobDefinitions: hasOutdatedLogEntryRateJobDefinitions,
hasStoppedJobs: hasStoppedLogEntryRateJobs,
moduleDescriptor: logEntryRateModuleDescriptor,
setupStatus: logEntryRateSetupStatus,
} = useLogEntryRateModuleContext();
const {
categoryQualityWarnings,
hasOutdatedJobConfigurations: hasOutdatedLogEntryCategoriesJobConfigurations,
hasOutdatedJobDefinitions: hasOutdatedLogEntryCategoriesJobDefinitions,
hasStoppedJobs: hasStoppedLogEntryCategoriesJobs,
moduleDescriptor: logEntryCategoriesModuleDescriptor,
setupStatus: logEntryCategoriesSetupStatus,
} = useLogEntryCategoriesModuleContext();
const {
timeRange: selectedTimeRange,
setTimeRange: setSelectedTimeRange,
@ -145,41 +150,33 @@ export const LogEntryRateResultsContent: React.FunctionComponent<LogEntryRateRes
[setAutoRefresh]
);
const viewSetupFlyoutForReconfiguration = useCallback(() => {
viewSetupForReconfiguration();
onOpenSetup();
}, [viewSetupForReconfiguration, onOpenSetup]);
const { showModuleList, showModuleSetup } = useLogAnalysisSetupFlyoutStateContext();
const viewSetupFlyoutForUpdate = useCallback(() => {
viewSetupForUpdate();
onOpenSetup();
}, [viewSetupForUpdate, onOpenSetup]);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const hasResults = useMemo(() => (logEntryRate?.histogramBuckets?.length ?? 0) > 0, [
logEntryRate,
const showLogEntryRateSetup = useCallback(() => showModuleSetup('logs_ui_analysis'), [
showModuleSetup,
]);
const showLogEntryCategoriesSetup = useCallback(() => showModuleSetup('logs_ui_categories'), [
showModuleSetup,
]);
const hasLogRateResults = (logEntryRate?.histogramBuckets?.length ?? 0) > 0;
const hasAnomalyResults = logEntryAnomalies.length > 0;
const isFirstUse = useMemo(
() =>
((setupStatus.type === 'skipped' && !!setupStatus.newlyCreated) ||
setupStatus.type === 'succeeded') &&
!hasResults,
[hasResults, setupStatus]
((logEntryCategoriesSetupStatus.type === 'skipped' &&
!!logEntryCategoriesSetupStatus.newlyCreated) ||
logEntryCategoriesSetupStatus.type === 'succeeded' ||
(logEntryRateSetupStatus.type === 'skipped' && !!logEntryRateSetupStatus.newlyCreated) ||
logEntryRateSetupStatus.type === 'succeeded') &&
!(hasLogRateResults || hasAnomalyResults),
[hasAnomalyResults, hasLogRateResults, logEntryCategoriesSetupStatus, logEntryRateSetupStatus]
);
useEffect(() => {
getLogEntryRate();
}, [getLogEntryRate, queryTimeRange.lastChangedTime]);
useEffect(() => {
fetchModuleDefinition();
}, [fetchModuleDefinition]);
useInterval(() => {
fetchJobStatus();
}, JOB_STATUS_POLLING_INTERVAL);
useInterval(
() => {
handleQueryTimeRangeChange({
@ -209,12 +206,23 @@ export const LogEntryRateResultsContent: React.FunctionComponent<LogEntryRateRes
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogAnalysisJobProblemIndicator
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasStoppedJobs={hasStoppedJobs}
hasOutdatedJobConfigurations={hasOutdatedLogEntryRateJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedLogEntryRateJobDefinitions}
hasStoppedJobs={hasStoppedLogEntryRateJobs}
isFirstUse={false /* the first use message is already shown by the section below */}
moduleName={logEntryRateModuleDescriptor.moduleName}
onRecreateMlJobForReconfiguration={showLogEntryRateSetup}
onRecreateMlJobForUpdate={showLogEntryRateSetup}
/>
<CategoryJobNoticesSection
hasOutdatedJobConfigurations={hasOutdatedLogEntryCategoriesJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedLogEntryCategoriesJobDefinitions}
hasStoppedJobs={hasStoppedLogEntryCategoriesJobs}
isFirstUse={isFirstUse}
onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration}
onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate}
moduleName={logEntryCategoriesModuleDescriptor.moduleName}
onRecreateMlJobForReconfiguration={showLogEntryCategoriesSetup}
onRecreateMlJobForUpdate={showLogEntryCategoriesSetup}
qualityWarnings={categoryQualityWarnings}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -222,7 +230,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<LogEntryRateRes
<AnomaliesResults
isLoadingLogRateResults={isLoading}
isLoadingAnomaliesResults={isLoadingLogEntryAnomalies}
viewSetupForReconfiguration={viewSetupFlyoutForReconfiguration}
onViewModuleList={showModuleList}
logEntryRateResults={logEntryRate}
anomalies={logEntryAnomalies}
setTimeRange={handleChartTimeRangeChange}

View file

@ -4,18 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiStat } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiTitle } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useMount } from 'react-use';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import { LogEntryAnomaly } from '../../../../../../common/http_api';
import { useLogEntryExamples } from '../../use_log_entry_examples';
import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples';
import { LogEntryExampleMessage, LogEntryExampleMessageHeaders } from './log_entry_example';
import { euiStyled } from '../../../../../../../observability/public';
import { LogEntryAnomaly } from '../../../../../../common/http_api';
import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples';
import { useLogSourceContext } from '../../../../../containers/logs/log_source';
import { useLogEntryExamples } from '../../use_log_entry_examples';
import { LogEntryExampleMessage, LogEntryExampleMessageHeaders } from './log_entry_example';
const EXAMPLE_COUNT = 5;

View file

@ -20,16 +20,16 @@ import { TimeRange } from '../../../../../../common/http_api/shared/time_range';
import { getAnnotationsForAll, getLogEntryRateCombinedSeries } from '../helpers/data_formatters';
import { AnomaliesChart } from './chart';
import { AnomaliesTable } from './table';
import { RecreateJobButton } from '../../../../../components/logging/log_analysis_job_status';
import { ManageJobsButton } from '../../../../../components/logging/log_analysis_setup/manage_jobs_button';
import {
Page,
ChangePaginationOptions,
ChangeSortOptions,
FetchNextPage,
FetchPreviousPage,
ChangeSortOptions,
ChangePaginationOptions,
SortOptions,
PaginationOptions,
LogEntryAnomalies,
Page,
PaginationOptions,
SortOptions,
} from '../../use_log_entry_anomalies_results';
import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper';
@ -40,7 +40,7 @@ export const AnomaliesResults: React.FunctionComponent<{
anomalies: LogEntryAnomalies;
setTimeRange: (timeRange: TimeRange) => void;
timeRange: TimeRange;
viewSetupForReconfiguration: () => void;
onViewModuleList: () => void;
page: Page;
fetchNextPage?: FetchNextPage;
fetchPreviousPage?: FetchPreviousPage;
@ -54,7 +54,7 @@ export const AnomaliesResults: React.FunctionComponent<{
logEntryRateResults,
setTimeRange,
timeRange,
viewSetupForReconfiguration,
onViewModuleList,
anomalies,
changeSortOptions,
sortOptions,
@ -93,7 +93,7 @@ export const AnomaliesResults: React.FunctionComponent<{
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RecreateJobButton onClick={viewSetupForReconfiguration} size="s" />
<ManageJobsButton onClick={onViewModuleList} size="s" />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />

View file

@ -42,10 +42,10 @@ export const LogsPageContent: React.FunctionComponent = () => {
pathname: '/stream',
};
const logRateTab = {
const anomaliesTab = {
app: 'logs',
title: logRateTabTitle,
pathname: '/log-rate',
title: anomaliesTabTitle,
pathname: '/anomalies',
};
const logCategoriesTab = {
@ -77,7 +77,7 @@ export const LogsPageContent: React.FunctionComponent = () => {
<AppNavigation aria-label={pageTitle}>
<EuiFlexGroup gutterSize={'none'} alignItems={'center'}>
<EuiFlexItem>
<RoutedTabs tabs={[streamTab, logRateTab, logCategoriesTab, settingsTab]} />
<RoutedTabs tabs={[streamTab, anomaliesTab, logCategoriesTab, settingsTab]} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AlertDropdown />
@ -96,10 +96,11 @@ export const LogsPageContent: React.FunctionComponent = () => {
</AppNavigation>
<Switch>
<Route path={streamTab.pathname} component={StreamPage} />
<Route path={logRateTab.pathname} component={LogEntryRatePage} />
<Route path={anomaliesTab.pathname} component={LogEntryRatePage} />
<Route path={logCategoriesTab.pathname} component={LogEntryCategoriesPage} />
<Route path={settingsTab.pathname} component={LogsSettingsPage} />
<RedirectWithQueryParams from={'/analysis'} to={logRateTab.pathname} exact />
<RedirectWithQueryParams from={'/analysis'} to={anomaliesTab.pathname} exact />
<RedirectWithQueryParams from={'/log-rate'} to={anomaliesTab.pathname} exact />
<RedirectWithQueryParams from={'/'} to={streamTab.pathname} exact />
</Switch>
</ColumnarPage>
@ -114,8 +115,8 @@ const streamTabTitle = i18n.translate('xpack.infra.logs.index.streamTabTitle', {
defaultMessage: 'Stream',
});
const logRateTabTitle = i18n.translate('xpack.infra.logs.index.logRateBetaBadgeTitle', {
defaultMessage: 'Log Rate',
const anomaliesTabTitle = i18n.translate('xpack.infra.logs.index.anomaliesTabTitle', {
defaultMessage: 'Anomalies',
});
const logCategoriesTabTitle = i18n.translate('xpack.infra.logs.index.logCategoriesBetaBadgeTitle', {

View file

@ -7469,14 +7469,9 @@
"xpack.infra.logs.alerting.threshold.fired": "実行",
"xpack.infra.logs.analysis.analyzeInMlButtonLabel": "ML で分析",
"xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "15 分ごとのログエントリー (平均)",
"xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "異常を読み込み中",
"xpack.infra.logs.analysis.anomaliesSectionTitle": "異常",
"xpack.infra.logs.analysis.anomalySectionNoDataBody": "時間範囲を調整する必要があるかもしれません。",
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。",
"xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutMessage": "異なるソース構成を使用して ML ジョブが作成されました。現在の構成を適用するにはジョブを再作成してください。これにより以前検出された異常が削除されます。",
"xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutTitle": "古い ML ジョブ構成",
"xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutMessage": "ML ジョブの新しいバージョンが利用可能です。新しいバージョンをデプロイするにはジョブを再作成してください。これにより以前検出された異常が削除されます。",
"xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutTitle": "古い ML ジョブ定義",
"xpack.infra.logs.analysis.jobStoppedCalloutMessage": "ML ジョブが手動またはリソース不足により停止しました。新しいログエントリーはジョブが再起動するまで処理されません。",
"xpack.infra.logs.analysis.jobStoppedCalloutTitle": "ML ジョブが停止しました",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesBody": "本機能は機械学習ジョブを利用し、そのステータスと結果にアクセスするためには、少なくとも{machineLearningUserRole}ロールが必要です。",
@ -7517,7 +7512,6 @@
"xpack.infra.logs.highlights.highlightsPopoverButtonLabel": "ハイライト",
"xpack.infra.logs.highlights.highlightTermsFieldLabel": "ハイライトする用語",
"xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "カテゴリー",
"xpack.infra.logs.index.logRateBetaBadgeTitle": "ログレート",
"xpack.infra.logs.index.settingsTabTitle": "設定",
"xpack.infra.logs.index.streamTabTitle": "ストリーム",
"xpack.infra.logs.jumpToTailText": "最も新しいエントリーに移動",

View file

@ -7474,14 +7474,9 @@
"xpack.infra.logs.alerting.threshold.fired": "已触发",
"xpack.infra.logs.analysis.analyzeInMlButtonLabel": "在 ML 中分析",
"xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "每 15 分钟日志条目数(平均值)",
"xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "正在加载异常",
"xpack.infra.logs.analysis.anomaliesSectionTitle": "异常",
"xpack.infra.logs.analysis.anomalySectionNoDataBody": "您可能想调整时间范围。",
"xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。",
"xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutMessage": "创建 ML 作业时所使用的源配置不同。重新创建作业以应用当前配置。这将移除以前检测到的异常。",
"xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutTitle": "ML 作业配置已过期",
"xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutMessage": "ML 作业有更新的版本可用。重新创建作业以部署更新的版本。这将移除以前检测到的异常。",
"xpack.infra.logs.analysis.jobDefinitionOutdatedCalloutTitle": "ML 作业定义已过期",
"xpack.infra.logs.analysis.jobStoppedCalloutMessage": "ML 作业已手动停止或由于缺乏资源而停止。作业重新启动后,才会处理新的日志条目。",
"xpack.infra.logs.analysis.jobStoppedCalloutTitle": "ML 作业已停止",
"xpack.infra.logs.analysis.missingMlResultsPrivilegesBody": "此功能使用 Machine Learning 作业,要访问这些作业的状态和结果,至少需要 {machineLearningUserRole} 角色。",
@ -7522,7 +7517,6 @@
"xpack.infra.logs.highlights.highlightsPopoverButtonLabel": "突出显示",
"xpack.infra.logs.highlights.highlightTermsFieldLabel": "要突出显示的词",
"xpack.infra.logs.index.logCategoriesBetaBadgeTitle": "类别",
"xpack.infra.logs.index.logRateBetaBadgeTitle": "日志速率",
"xpack.infra.logs.index.settingsTabTitle": "设置",
"xpack.infra.logs.index.streamTabTitle": "流式传输",
"xpack.infra.logs.jumpToTailText": "跳到最近的条目",