[Logs UI] View log details for anomaly log examples (#75425)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Gómez 2020-08-26 10:38:54 +02:00 committed by GitHub
parent ddf99b64db
commit 686cde88af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 186 additions and 110 deletions

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
export { LogEntryFlyout } from './log_entry_flyout'; export * from './log_entry_flyout';

View file

@ -26,12 +26,10 @@ import { InfraLoadingPanel } from '../../loading';
import { LogEntryActionsMenu } from './log_entry_actions_menu'; import { LogEntryActionsMenu } from './log_entry_actions_menu';
import { LogEntriesItem, LogEntriesItemField } from '../../../../common/http_api'; import { LogEntriesItem, LogEntriesItemField } from '../../../../common/http_api';
interface Props { export interface LogEntryFlyoutProps {
flyoutItem: LogEntriesItem | null; flyoutItem: LogEntriesItem | null;
setFlyoutVisibility: (visible: boolean) => void; setFlyoutVisibility: (visible: boolean) => void;
setFilter: (filter: string) => void; setFilter: (filter: string, flyoutItemId: string, timeKey?: TimeKey) => void;
setTarget: (timeKey: TimeKey, flyoutItemId: string) => void;
loading: boolean; loading: boolean;
} }
@ -40,27 +38,27 @@ export const LogEntryFlyout = ({
loading, loading,
setFlyoutVisibility, setFlyoutVisibility,
setFilter, setFilter,
setTarget, }: LogEntryFlyoutProps) => {
}: Props) => {
const createFilterHandler = useCallback( const createFilterHandler = useCallback(
(field: LogEntriesItemField) => () => { (field: LogEntriesItemField) => () => {
const filter = `${field.field}:"${field.value}"`; if (!flyoutItem) {
setFilter(filter); return;
if (flyoutItem && flyoutItem.key) {
const timestampMoment = moment(flyoutItem.key.time);
if (timestampMoment.isValid()) {
setTarget(
{
time: timestampMoment.valueOf(),
tiebreaker: flyoutItem.key.tiebreaker,
},
flyoutItem.id
);
}
} }
const filter = `${field.field}:"${field.value}"`;
const timestampMoment = moment(flyoutItem.key.time);
let target;
if (timestampMoment.isValid()) {
target = {
time: timestampMoment.valueOf(),
tiebreaker: flyoutItem.key.tiebreaker,
};
}
setFilter(filter, flyoutItem.id, target);
}, },
[flyoutItem, setFilter, setTarget] [flyoutItem, setFilter]
); );
const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]); const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]);

View file

@ -10,6 +10,7 @@ import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_a
import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space'; import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
import { LogFlyout } from '../../../containers/logs/log_flyout';
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
const { sourceId, sourceConfiguration } = useLogSourceContext(); const { sourceId, sourceConfiguration } = useLogSourceContext();
@ -23,20 +24,22 @@ export const LogEntryRatePageProviders: React.FunctionComponent = ({ children })
} }
return ( return (
<LogEntryRateModuleProvider <LogFlyout.Provider>
indexPattern={sourceConfiguration?.configuration.logAlias ?? ''} <LogEntryRateModuleProvider
sourceId={sourceId}
spaceId={space.id}
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
>
<LogEntryCategoriesModuleProvider
indexPattern={sourceConfiguration?.configuration.logAlias ?? ''} indexPattern={sourceConfiguration?.configuration.logAlias ?? ''}
sourceId={sourceId} sourceId={sourceId}
spaceId={space.id} spaceId={space.id}
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''} timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
> >
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider> <LogEntryCategoriesModuleProvider
</LogEntryCategoriesModuleProvider> indexPattern={sourceConfiguration?.configuration.logAlias ?? ''}
</LogEntryRateModuleProvider> sourceId={sourceId}
spaceId={space.id}
timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''}
>
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider>
</LogEntryCategoriesModuleProvider>
</LogEntryRateModuleProvider>
</LogFlyout.Provider>
); );
}; };

View file

@ -7,7 +7,9 @@
import datemath from '@elastic/datemath'; import datemath from '@elastic/datemath';
import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui';
import moment from 'moment'; import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { encode, RisonValue } from 'rison-node';
import { stringify } from 'query-string';
import React, { useCallback, useEffect, useMemo, useState, useContext } from 'react';
import { euiStyled, useTrackPageview } from '../../../../../observability/public'; import { euiStyled, useTrackPageview } from '../../../../../observability/public';
import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { TimeRange } from '../../../../common/http_api/shared/time_range';
import { bucketSpan } from '../../../../common/log_analysis'; import { bucketSpan } from '../../../../common/log_analysis';
@ -29,6 +31,9 @@ import {
StringTimeRange, StringTimeRange,
useLogAnalysisResultsUrlState, useLogAnalysisResultsUrlState,
} from './use_log_entry_rate_results_url_state'; } from './use_log_entry_rate_results_url_state';
import { LogEntryFlyout, LogEntryFlyoutProps } from '../../../components/logging/log_entry_flyout';
import { LogFlyout } from '../../../containers/logs/log_flyout';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
export const SORT_DEFAULTS = { export const SORT_DEFAULTS = {
direction: 'desc' as const, direction: 'desc' as const,
@ -42,6 +47,7 @@ export const PAGINATION_DEFAULTS = {
export const LogEntryRateResultsContent: React.FunctionComponent = () => { export const LogEntryRateResultsContent: React.FunctionComponent = () => {
useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' });
useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 });
const navigateToApp = useKibana().services.application?.navigateToApp;
const { sourceId } = useLogSourceContext(); const { sourceId } = useLogSourceContext();
@ -79,6 +85,30 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
lastChangedTime: Date.now(), lastChangedTime: Date.now(),
})); }));
const linkToLogStream = useCallback<LogEntryFlyoutProps['setFilter']>(
(filter, id, timeKey) => {
const params = {
logPosition: encode({
end: moment(queryTimeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
position: timeKey as RisonValue,
start: moment(queryTimeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
streamLive: false,
}),
flyoutOptions: encode({
surroundingLogsId: id,
}),
logFilter: encode({
expression: filter,
kind: 'kuery',
}),
};
// eslint-disable-next-line no-unused-expressions
navigateToApp?.('logs', { path: `/stream?${stringify(params)}` });
},
[queryTimeRange, navigateToApp]
);
const bucketDuration = useMemo( const bucketDuration = useMemo(
() => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime), () => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime),
[queryTimeRange.value.endTime, queryTimeRange.value.startTime] [queryTimeRange.value.endTime, queryTimeRange.value.startTime]
@ -115,6 +145,10 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
filteredDatasets: selectedDatasets, filteredDatasets: selectedDatasets,
}); });
const { flyoutVisible, setFlyoutVisibility, flyoutItem, isLoading: isFlyoutLoading } = useContext(
LogFlyout.Context
);
const handleQueryTimeRangeChange = useCallback( const handleQueryTimeRangeChange = useCallback(
({ start: startTime, end: endTime }: { start: string; end: string }) => { ({ start: startTime, end: endTime }: { start: string; end: string }) => {
setQueryTimeRange({ setQueryTimeRange({
@ -198,75 +232,86 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => {
); );
return ( return (
<ResultsContentPage> <>
<EuiFlexGroup direction="column"> <ResultsContentPage>
<EuiFlexItem grow={false}> <EuiFlexGroup direction="column">
<EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexItem grow={false}>
<EuiFlexItem> <EuiFlexGroup justifyContent="spaceBetween">
<DatasetsSelector <EuiFlexItem>
availableDatasets={datasets} <DatasetsSelector
isLoading={isLoadingDatasets} availableDatasets={datasets}
selectedDatasets={selectedDatasets} isLoading={isLoadingDatasets}
onChangeDatasetSelection={setSelectedDatasets} selectedDatasets={selectedDatasets}
/> onChangeDatasetSelection={setSelectedDatasets}
</EuiFlexItem> />
<EuiFlexItem grow={false}> </EuiFlexItem>
<EuiSuperDatePicker <EuiFlexItem grow={false}>
start={selectedTimeRange.startTime} <EuiSuperDatePicker
end={selectedTimeRange.endTime} start={selectedTimeRange.startTime}
onTimeChange={handleSelectedTimeRangeChange} end={selectedTimeRange.endTime}
isPaused={autoRefresh.isPaused} onTimeChange={handleSelectedTimeRangeChange}
refreshInterval={autoRefresh.interval} isPaused={autoRefresh.isPaused}
onRefreshChange={handleAutoRefreshChange} refreshInterval={autoRefresh.interval}
/> onRefreshChange={handleAutoRefreshChange}
</EuiFlexItem> />
</EuiFlexGroup> </EuiFlexItem>
</EuiFlexItem> </EuiFlexGroup>
<EuiFlexItem grow={false}> </EuiFlexItem>
<LogAnalysisJobProblemIndicator <EuiFlexItem grow={false}>
hasOutdatedJobConfigurations={hasOutdatedLogEntryRateJobConfigurations} <LogAnalysisJobProblemIndicator
hasOutdatedJobDefinitions={hasOutdatedLogEntryRateJobDefinitions} hasOutdatedJobConfigurations={hasOutdatedLogEntryRateJobConfigurations}
hasSetupCapabilities={hasLogAnalysisSetupCapabilities} hasOutdatedJobDefinitions={hasOutdatedLogEntryRateJobDefinitions}
hasStoppedJobs={hasStoppedLogEntryRateJobs} hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
isFirstUse={false /* the first use message is already shown by the section below */} hasStoppedJobs={hasStoppedLogEntryRateJobs}
moduleName={logEntryRateModuleDescriptor.moduleName} isFirstUse={false /* the first use message is already shown by the section below */}
onRecreateMlJobForReconfiguration={showLogEntryRateSetup} moduleName={logEntryRateModuleDescriptor.moduleName}
onRecreateMlJobForUpdate={showLogEntryRateSetup} onRecreateMlJobForReconfiguration={showLogEntryRateSetup}
/> onRecreateMlJobForUpdate={showLogEntryRateSetup}
<CategoryJobNoticesSection
hasOutdatedJobConfigurations={hasOutdatedLogEntryCategoriesJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedLogEntryCategoriesJobDefinitions}
hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
hasStoppedJobs={hasStoppedLogEntryCategoriesJobs}
isFirstUse={isFirstUse}
moduleName={logEntryCategoriesModuleDescriptor.moduleName}
onRecreateMlJobForReconfiguration={showLogEntryCategoriesSetup}
onRecreateMlJobForUpdate={showLogEntryCategoriesSetup}
qualityWarnings={categoryQualityWarnings}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<AnomaliesResults
isLoadingLogRateResults={isLoading}
isLoadingAnomaliesResults={isLoadingLogEntryAnomalies}
onViewModuleList={showModuleList}
logEntryRateResults={logEntryRate}
anomalies={logEntryAnomalies}
setTimeRange={handleChartTimeRangeChange}
timeRange={queryTimeRange.value}
page={page}
fetchNextPage={fetchNextPage}
fetchPreviousPage={fetchPreviousPage}
changeSortOptions={changeSortOptions}
changePaginationOptions={changePaginationOptions}
sortOptions={sortOptions}
paginationOptions={paginationOptions}
/> />
</EuiPanel> <CategoryJobNoticesSection
</EuiFlexItem> hasOutdatedJobConfigurations={hasOutdatedLogEntryCategoriesJobConfigurations}
</EuiFlexGroup> hasOutdatedJobDefinitions={hasOutdatedLogEntryCategoriesJobDefinitions}
</ResultsContentPage> hasSetupCapabilities={hasLogAnalysisSetupCapabilities}
hasStoppedJobs={hasStoppedLogEntryCategoriesJobs}
isFirstUse={isFirstUse}
moduleName={logEntryCategoriesModuleDescriptor.moduleName}
onRecreateMlJobForReconfiguration={showLogEntryCategoriesSetup}
onRecreateMlJobForUpdate={showLogEntryCategoriesSetup}
qualityWarnings={categoryQualityWarnings}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<AnomaliesResults
isLoadingLogRateResults={isLoading}
isLoadingAnomaliesResults={isLoadingLogEntryAnomalies}
onViewModuleList={showModuleList}
logEntryRateResults={logEntryRate}
anomalies={logEntryAnomalies}
setTimeRange={handleChartTimeRangeChange}
timeRange={queryTimeRange.value}
page={page}
fetchNextPage={fetchNextPage}
fetchPreviousPage={fetchPreviousPage}
changeSortOptions={changeSortOptions}
changePaginationOptions={changePaginationOptions}
sortOptions={sortOptions}
paginationOptions={paginationOptions}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</ResultsContentPage>
{flyoutVisible ? (
<LogEntryFlyout
flyoutItem={flyoutItem}
setFlyoutVisibility={setFlyoutVisibility}
loading={isFlyoutLoading}
setFilter={linkToLogStream}
/>
) : null}
</>
); );
}; };

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import React, { useMemo, useCallback, useState } from 'react'; import React, { useMemo, useCallback, useState, useContext } from 'react';
import moment from 'moment'; import moment from 'moment';
import { encode } from 'rison-node'; import { encode } from 'rison-node';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -37,6 +37,7 @@ import {
} from '../../../../../utils/source_configuration'; } from '../../../../../utils/source_configuration';
import { localizedDate } from '../../../../../../common/formatters/datetime'; import { localizedDate } from '../../../../../../common/formatters/datetime';
import { LogEntryAnomaly } from '../../../../../../common/http_api'; import { LogEntryAnomaly } from '../../../../../../common/http_api';
import { LogFlyout } from '../../../../../containers/logs/log_flyout';
export const exampleMessageScale = 'medium' as const; export const exampleMessageScale = 'medium' as const;
export const exampleTimestampFormat = 'time' as const; export const exampleTimestampFormat = 'time' as const;
@ -45,6 +46,13 @@ const MENU_LABEL = i18n.translate('xpack.infra.logAnomalies.logEntryExamplesMenu
defaultMessage: 'View actions for log entry', defaultMessage: 'View actions for log entry',
}); });
const VIEW_DETAILS_LABEL = i18n.translate(
'xpack.infra.logs.analysis.logEntryExamplesViewDetailsLabel',
{
defaultMessage: 'View details',
}
);
const VIEW_IN_STREAM_LABEL = i18n.translate( const VIEW_IN_STREAM_LABEL = i18n.translate(
'xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel', 'xpack.infra.logs.analysis.logEntryExamplesViewInStreamLabel',
{ {
@ -80,6 +88,8 @@ export const LogEntryExampleMessage: React.FunctionComponent<Props> = ({
const setItemIsHovered = useCallback(() => setIsHovered(true), []); const setItemIsHovered = useCallback(() => setIsHovered(true), []);
const setItemIsNotHovered = useCallback(() => setIsHovered(false), []); const setItemIsNotHovered = useCallback(() => setIsHovered(false), []);
const { setFlyoutVisibility, setFlyoutId } = useContext(LogFlyout.Context);
// handle special cases for the dataset value // handle special cases for the dataset value
const humanFriendlyDataset = getFriendlyNameForPartitionId(dataset); const humanFriendlyDataset = getFriendlyNameForPartitionId(dataset);
@ -116,6 +126,13 @@ export const LogEntryExampleMessage: React.FunctionComponent<Props> = ({
} }
return [ return [
{
label: VIEW_DETAILS_LABEL,
onClick: () => {
setFlyoutId(id);
setFlyoutVisibility(true);
},
},
{ {
label: VIEW_IN_STREAM_LABEL, label: VIEW_IN_STREAM_LABEL,
onClick: viewInStreamLinkProps.onClick, onClick: viewInStreamLinkProps.onClick,
@ -127,7 +144,13 @@ export const LogEntryExampleMessage: React.FunctionComponent<Props> = ({
href: viewAnomalyInMachineLearningLinkProps.href, href: viewAnomalyInMachineLearningLinkProps.href,
}, },
]; ];
}, [viewInStreamLinkProps, viewAnomalyInMachineLearningLinkProps]); }, [
id,
setFlyoutId,
setFlyoutVisibility,
viewInStreamLinkProps,
viewAnomalyInMachineLearningLinkProps,
]);
return ( return (
<LogEntryRowWrapper <LogEntryRowWrapper

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import React, { useContext } from 'react'; import React, { useContext, useCallback } from 'react';
import { euiStyled } from '../../../../../observability/public'; import { euiStyled } from '../../../../../observability/public';
import { AutoSizer } from '../../../components/auto_sizer'; import { AutoSizer } from '../../../components/auto_sizer';
import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout'; import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout';
@ -57,6 +57,18 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
const [, { setContextEntry }] = useContext(ViewLogInContext.Context); const [, { setContextEntry }] = useContext(ViewLogInContext.Context);
const setFilter = useCallback(
(filter, flyoutItemId, timeKey) => {
applyLogFilterQuery(filter);
if (timeKey) {
jumpToTargetPosition(timeKey);
}
setSurroundingLogsId(flyoutItemId);
stopLiveStreaming();
},
[applyLogFilterQuery, jumpToTargetPosition, setSurroundingLogsId, stopLiveStreaming]
);
return ( return (
<> <>
<WithLogTextviewUrlState /> <WithLogTextviewUrlState />
@ -65,12 +77,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
<PageViewLogInContext /> <PageViewLogInContext />
{flyoutVisible ? ( {flyoutVisible ? (
<LogEntryFlyout <LogEntryFlyout
setFilter={applyLogFilterQuery} setFilter={setFilter}
setTarget={(timeKey, flyoutItemId) => {
jumpToTargetPosition(timeKey);
setSurroundingLogsId(flyoutItemId);
stopLiveStreaming();
}}
setFlyoutVisibility={setFlyoutVisibility} setFlyoutVisibility={setFlyoutVisibility}
flyoutItem={flyoutItem} flyoutItem={flyoutItem}
loading={isLoading} loading={isLoading}