[Logs UI] Add log analysis tab (#42931) (#43253)

* Add empty analysis tab

* Add ml capabilities check

* Add job status checking functionality

* Add a loading page for the job status check

* Change types / change method for deriving space ID / change setup requirement filtering check

* Use new structure

* Change tab syntax

* i18n translate message prop

* Fix import
This commit is contained in:
Kerry Gallagher 2019-08-14 11:41:15 +01:00 committed by GitHub
parent 9f0b7ce998
commit 015116980d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 468 additions and 88 deletions

View file

@ -6,5 +6,8 @@
import { JobType } from './log_analysis';
export const getJobIdPrefix = (spaceId: string, sourceId: string) =>
`kibana-logs-ui-${spaceId}-${sourceId}-`;
export const getJobId = (spaceId: string, sourceId: string, jobType: JobType) =>
`kibana-logs-ui-${spaceId}-${sourceId}-${jobType}`;
`${getJobIdPrefix(spaceId, sourceId)}${jobType}`;

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 * as rt from 'io-ts';
import { kfetch } from 'ui/kfetch';
import { getJobId } from '../../../../../common/log_analysis';
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
export const callJobsSummaryAPI = async (spaceId: string, sourceId: string) => {
const response = await kfetch({
method: 'POST',
pathname: '/api/ml/jobs/jobs_summary',
body: JSON.stringify(
fetchJobStatusRequestPayloadRT.encode({
jobIds: [getJobId(spaceId, sourceId, 'log-entry-rate')],
})
),
});
return fetchJobStatusResponsePayloadRT.decode(response).getOrElseL(throwErrors(createPlainError));
};
export const fetchJobStatusRequestPayloadRT = rt.type({
jobIds: rt.array(rt.string),
});
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;
export type FetchJobStatusResponsePayload = rt.TypeOf<typeof fetchJobStatusResponsePayloadRT>;

View file

@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './log_analysis_capabilities';
export * from './log_analysis_jobs';
export * from './log_analysis_results';

View file

@ -0,0 +1,85 @@
/*
* 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 { useMemo, useState, useEffect } from 'react';
import { kfetch } from 'ui/kfetch';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import {
getMlCapabilitiesResponsePayloadRT,
GetMlCapabilitiesResponsePayload,
} from './ml_api_types';
import { throwErrors, createPlainError } from '../../../../common/runtime_types';
export const useLogAnalysisCapabilities = () => {
const [mlCapabilities, setMlCapabilities] = useState<GetMlCapabilitiesResponsePayload>(
initialMlCapabilities
);
const [fetchMlCapabilitiesRequest, fetchMlCapabilities] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
const rawResponse = await kfetch({
method: 'GET',
pathname: '/api/ml/ml_capabilities',
});
return getMlCapabilitiesResponsePayloadRT
.decode(rawResponse)
.getOrElseL(throwErrors(createPlainError));
},
onResolve: response => {
setMlCapabilities(response);
},
},
[]
);
useEffect(() => {
fetchMlCapabilities();
}, []);
const isLoading = useMemo(() => fetchMlCapabilitiesRequest.state === 'pending', [
fetchMlCapabilitiesRequest.state,
]);
return {
hasLogAnalysisCapabilites: mlCapabilities.capabilities.canCreateJob,
isLoading,
};
};
const initialMlCapabilities = {
capabilities: {
canGetJobs: false,
canCreateJob: false,
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
canForecastJob: false,
canGetDatafeeds: false,
canStartStopDatafeed: false,
canUpdateJob: false,
canUpdateDatafeed: false,
canPreviewDatafeed: false,
canGetCalendars: false,
canCreateCalendar: false,
canDeleteCalendar: false,
canGetFilters: false,
canCreateFilter: false,
canDeleteFilter: false,
canFindFileStructure: false,
canGetDataFrameJobs: false,
canDeleteDataFrameJob: false,
canPreviewDataFrameJob: false,
canCreateDataFrameJob: false,
canStartStopDataFrameJob: false,
},
isPlatinumOrTrialLicense: false,
mlFeatureEnabledInSpace: false,
upgradeInProgress: false,
};

View file

@ -0,0 +1,97 @@
/*
* 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-latest';
import { useMemo, useEffect, useState } from 'react';
import { values } from 'lodash';
import { getJobId } from '../../../../common/log_analysis';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
import { callJobsSummaryAPI } from './api/ml_get_jobs_summary_api';
type JobStatus = 'unknown' | 'closed' | 'closing' | 'failed' | 'opened' | 'opening' | 'deleted';
// type DatafeedStatus = 'unknown' | 'started' | 'starting' | 'stopped' | 'stopping' | 'deleted';
export const useLogAnalysisJobs = ({
indexPattern,
sourceId,
spaceId,
}: {
indexPattern: string;
sourceId: string;
spaceId: string;
}) => {
const [jobStatus, setJobStatus] = useState<{
logEntryRate: JobStatus;
}>({
logEntryRate: 'unknown',
});
// const [setupMlModuleRequest, setupMlModule] = useTrackedPromise(
// {
// cancelPreviousOn: 'resolution',
// createPromise: async () => {
// kfetch({
// method: 'POST',
// pathname: '/api/ml/modules/setup',
// body: JSON.stringify(
// setupMlModuleRequestPayloadRT.encode({
// indexPatternName: indexPattern,
// prefix: getJobIdPrefix(spaceId, sourceId),
// startDatafeed: true,
// })
// ),
// });
// },
// },
// [indexPattern, spaceId, sourceId]
// );
const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise(
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
return 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',
});
}
},
onReject: error => {
// TODO: Handle errors
},
},
[indexPattern, spaceId, sourceId]
);
useEffect(() => {
fetchJobStatus();
}, []);
const isSetupRequired = useMemo(() => {
const jobStates = values(jobStatus);
return (
jobStates.filter(state => state === 'opened' || state === 'opening').length < jobStates.length
);
}, [jobStatus]);
const isLoadingSetupStatus = useMemo(() => fetchJobStatusRequest.state === 'pending', [
fetchJobStatusRequest.state,
]);
return {
jobStatus,
isSetupRequired,
isLoadingSetupStatus,
};
};
export const LogAnalysisJobs = createContainer(useLogAnalysisJobs);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import createContainer from 'constate-latest/dist/ts/src';
import createContainer from 'constate-latest';
import { useMemo } from 'react';
import { useLogEntryRate } from './log_entry_rate';

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as rt from 'io-ts';
export const getMlCapabilitiesResponsePayloadRT = rt.type({
capabilities: rt.type({
canGetJobs: rt.boolean,
canCreateJob: rt.boolean,
canDeleteJob: rt.boolean,
canOpenJob: rt.boolean,
canCloseJob: rt.boolean,
canForecastJob: rt.boolean,
canGetDatafeeds: rt.boolean,
canStartStopDatafeed: rt.boolean,
canUpdateJob: rt.boolean,
canUpdateDatafeed: rt.boolean,
canPreviewDatafeed: rt.boolean,
}),
isPlatinumOrTrialLicense: rt.boolean,
mlFeatureEnabledInSpace: rt.boolean,
upgradeInProgress: rt.boolean,
});
export type GetMlCapabilitiesResponsePayload = rt.TypeOf<typeof getMlCapabilitiesResponsePayloadRT>;

View file

@ -0,0 +1,7 @@
/*
* 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 './page';

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useContext } from 'react';
import chrome from 'ui/chrome';
import { i18n } from '@kbn/i18n';
import { ColumnarPage } from '../../../components/page';
import { LoadingPage } from '../../../components/loading_page';
import { AnalysisPageProviders } from './page_providers';
import { AnalysisResultsContent } from './page_results_content';
import { AnalysisSetupContent } from './page_setup_content';
import { useLogAnalysisJobs } from '../../../containers/logs/log_analysis/log_analysis_jobs';
import { Source } from '../../../containers/source';
export const AnalysisPage = () => {
const { sourceId, source } = useContext(Source.Context);
const spaceId = chrome.getInjected('activeSpace').space.id;
const { isSetupRequired, isLoadingSetupStatus } = useLogAnalysisJobs({
indexPattern: source ? source.configuration.logAlias : '',
sourceId,
spaceId,
});
return (
<AnalysisPageProviders>
<ColumnarPage data-test-subj="infraLogsAnalysisPage">
{isLoadingSetupStatus ? (
<LoadingPage
message={i18n.translate('xpack.infra.logs.analysisPage.loadingMessage', {
defaultMessage: 'Checking status of analysis jobs...',
})}
/>
) : isSetupRequired ? (
<AnalysisSetupContent />
) : (
<AnalysisResultsContent />
)}
</ColumnarPage>
</AnalysisPageProviders>
);
};

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { Source, useSource } from '../../../containers/source';
import { useSourceId } from '../../../containers/source_id';
export const AnalysisPageProviders: React.FunctionComponent = ({ children }) => {
const [sourceId] = useSourceId();
const source = useSource({ sourceId });
return <Source.Context.Provider value={source}>{children}</Source.Context.Provider>;
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { useTrackPageview } from '../../../hooks/use_track_metric';
export const AnalysisResultsContent = () => {
useTrackPageview({ app: 'infra_logs', path: 'analysis_results' });
useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 });
return <div>Results</div>;
};

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { useTrackPageview } from '../../../hooks/use_track_metric';
export const AnalysisSetupContent = () => {
useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' });
useTrackPageview({ app: 'infra_logs', path: 'analysis_setup', delay: 15000 });
return <div>Setup</div>;
};

View file

@ -16,10 +16,15 @@ import { HelpCenterContent } from '../../components/help_center_content';
import { Header } from '../../components/header';
import { RoutedTabs } from '../../components/navigation/routed_tabs';
import { ColumnarPage } from '../../components/page';
import { Source } from '../../containers/source';
import { SourceLoadingPage } from '../../components/source_loading_page';
import { SourceErrorPage } from '../../components/source_error_page';
import { Source, useSource } from '../../containers/source';
import { StreamPage } from './stream';
import { SettingsPage } from '../shared/settings';
import { AppNavigation } from '../../components/navigation/app_navigation';
import { AnalysisPage } from './analysis';
import { useLogAnalysisCapabilities } from '../../containers/logs/log_analysis';
import { useSourceId } from '../../containers/source_id';
interface LogsPageProps extends RouteComponentProps {
intl: InjectedIntl;
@ -27,61 +32,90 @@ interface LogsPageProps extends RouteComponentProps {
}
export const LogsPage = injectUICapabilities(
injectI18n(({ match, intl, uiCapabilities }: LogsPageProps) => (
<Source.Provider sourceId="default">
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logs.index.documentTitle',
defaultMessage: 'Logs',
})}
/>
injectI18n(({ match, intl, uiCapabilities }: LogsPageProps) => {
const [sourceId] = useSourceId();
const source = useSource({ sourceId });
const { hasLogAnalysisCapabilites } = useLogAnalysisCapabilities();
const streamTab = {
title: intl.formatMessage({
id: 'xpack.infra.logs.index.streamTabTitle',
defaultMessage: 'Stream',
}),
path: `${match.path}/stream`,
};
const analysisTab = {
title: intl.formatMessage({
id: 'xpack.infra.logs.index.analysisTabTitle',
defaultMessage: 'Analysis',
}),
path: `${match.path}/analysis`,
};
const settingsTab = {
title: intl.formatMessage({
id: 'xpack.infra.logs.index.settingsTabTitle',
defaultMessage: 'Settings',
}),
path: `${match.path}/settings`,
};
return (
<Source.Context.Provider value={source}>
<ColumnarPage>
<DocumentTitle
title={intl.formatMessage({
id: 'xpack.infra.logs.index.documentTitle',
defaultMessage: 'Logs',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<HelpCenterContent
feedbackLink="https://discuss.elastic.co/c/logs"
feedbackLinkText={intl.formatMessage({
id: 'xpack.infra.logsPage.logsHelpContent.feedbackLinkText',
defaultMessage: 'Provide feedback for Logs',
})}
/>
<Header
breadcrumbs={[
{
text: i18n.translate('xpack.infra.header.logsTitle', {
defaultMessage: 'Logs',
}),
},
]}
readOnlyBadge={!uiCapabilities.logs.save}
/>
<AppNavigation>
<RoutedTabs
tabs={[
<Header
breadcrumbs={[
{
title: intl.formatMessage({
id: 'xpack.infra.logs.index.streamTabTitle',
defaultMessage: 'Stream',
text: i18n.translate('xpack.infra.header.logsTitle', {
defaultMessage: 'Logs',
}),
path: `${match.path}/stream`,
},
{
title: intl.formatMessage({
id: 'xpack.infra.logs.index.settingsTabTitle',
defaultMessage: 'Settings',
}),
path: `${match.path}/settings`,
},
]}
readOnlyBadge={!uiCapabilities.logs.save}
/>
</AppNavigation>
{source.isLoadingSource ||
(!source.isLoadingSource &&
!source.hasFailedLoadingSource &&
source.source === undefined) ? (
<SourceLoadingPage />
) : source.hasFailedLoadingSource ? (
<SourceErrorPage
errorMessage={source.loadSourceFailureMessage || ''}
retry={source.loadSource}
/>
) : (
<>
<AppNavigation>
<RoutedTabs
tabs={
hasLogAnalysisCapabilites
? [streamTab, analysisTab, settingsTab]
: [streamTab, settingsTab]
}
/>
</AppNavigation>
<Switch>
<Route path={`${match.path}/stream`} component={StreamPage} />
<Route path={`${match.path}/settings`} component={SettingsPage} />
</Switch>
</ColumnarPage>
</Source.Provider>
))
<Switch>
<Route path={`${match.path}/stream`} component={StreamPage} />
<Route path={`${match.path}/analysis`} component={AnalysisPage} />
<Route path={`${match.path}/settings`} component={SettingsPage} />
</Switch>
</>
)}
</ColumnarPage>
</Source.Context.Provider>
);
})
);

View file

@ -6,32 +6,12 @@
import React, { useContext } from 'react';
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { Source } from '../../../containers/source';
import { LogsPageLogsContent } from './page_logs_content';
import { LogsPageNoIndicesContent } from './page_no_indices_content';
export const StreamPageContent: React.FunctionComponent = () => {
const {
hasFailedLoadingSource,
isLoadingSource,
logIndicesExist,
loadSource,
loadSourceFailureMessage,
} = useContext(Source.Context);
const { logIndicesExist } = useContext(Source.Context);
return (
<>
{isLoadingSource ? (
<SourceLoadingPage />
) : logIndicesExist ? (
<LogsPageLogsContent />
) : hasFailedLoadingSource ? (
<SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} />
) : (
<LogsPageNoIndicesContent />
)}
</>
);
return <>{logIndicesExist ? <LogsPageLogsContent /> : <LogsPageNoIndicesContent />}</>;
};

View file

@ -4,27 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useContext } from 'react';
import { LogFlyout } from '../../../containers/logs/log_flyout';
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights';
import { Source, useSource } from '../../../containers/source';
import { useSourceId } from '../../../containers/source_id';
import { Source } from '../../../containers/source';
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
const [sourceId] = useSourceId();
const source = useSource({ sourceId });
const { sourceId, version } = useContext(Source.Context);
return (
<Source.Context.Provider value={source}>
<LogViewConfiguration.Provider>
<LogFlyout.Provider>
<LogHighlightsState.Provider sourceId={sourceId} sourceVersion={source.version}>
{children}
</LogHighlightsState.Provider>
</LogFlyout.Provider>
</LogViewConfiguration.Provider>
</Source.Context.Provider>
<LogViewConfiguration.Provider>
<LogFlyout.Provider>
<LogHighlightsState.Provider sourceId={sourceId} sourceVersion={version}>
{children}
</LogHighlightsState.Provider>
</LogFlyout.Provider>
</LogViewConfiguration.Provider>
);
};