Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
timeRangeRT,
routeTimingMetadataRT,
} from '../../shared';
import { logEntryContextRT } from '../../log_entries';

export const LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH =
'/api/infra/log_analysis/results/log_entry_category_examples';
Expand Down Expand Up @@ -42,9 +43,12 @@ export type GetLogEntryCategoryExamplesRequestPayload = rt.TypeOf<
*/

const logEntryCategoryExampleRT = rt.type({
id: rt.string,
dataset: rt.string,
message: rt.string,
timestamp: rt.number,
tiebreaker: rt.number,
context: logEntryContextRT,
});

export type LogEntryCategoryExample = rt.TypeOf<typeof logEntryCategoryExampleRT>;
Expand Down
13 changes: 8 additions & 5 deletions x-pack/plugins/infra/common/http_api/log_entries/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,17 @@ export const logMessageColumnRT = rt.type({

export const logColumnRT = rt.union([logTimestampColumnRT, logFieldColumnRT, logMessageColumnRT]);

export const logEntryContextRT = rt.union([
rt.type({}),
rt.type({ 'container.id': rt.string }),
rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }),
]);

export const logEntryRT = rt.type({
id: rt.string,
cursor: logEntriesCursorRT,
columns: rt.array(logColumnRT),
context: rt.union([
rt.type({}),
rt.type({ 'container.id': rt.string }),
rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }),
]),
context: logEntryContextRT,
});

export type LogMessageConstantPart = rt.TypeOf<typeof logMessageConstantPartRT>;
Expand All @@ -92,6 +94,7 @@ export type LogTimestampColumn = rt.TypeOf<typeof logTimestampColumnRT>;
export type LogFieldColumn = rt.TypeOf<typeof logFieldColumnRT>;
export type LogMessageColumn = rt.TypeOf<typeof logMessageColumnRT>;
export type LogColumn = rt.TypeOf<typeof logColumnRT>;
export type LogEntryContext = rt.TypeOf<typeof logEntryContextRT>;
export type LogEntry = rt.TypeOf<typeof logEntryRT>;

export const logEntriesResponseRT = rt.type({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ 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;

Expand Down Expand Up @@ -178,54 +180,61 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent<LogEntryC
);

return (
<ResultsContentPage>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem />
<EuiFlexItem grow={false}>
<EuiSuperDatePicker
start={selectedTimeRange.startTime}
end={selectedTimeRange.endTime}
onTimeChange={handleSelectedTimeRangeChange}
isPaused={autoRefresh.isPaused}
refreshInterval={autoRefresh.interval}
onRefreshChange={handleAutoRefreshChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CategoryJobNoticesSection
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasStoppedJobs={hasStoppedJobs}
isFirstUse={isFirstUse}
onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration}
onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate}
qualityWarnings={categoryQualityWarnings}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<TopCategoriesSection
availableDatasets={logEntryCategoryDatasets}
isLoadingDatasets={isLoadingLogEntryCategoryDatasets}
isLoadingTopCategories={isLoadingTopLogEntryCategories}
jobId={jobIds['log-entry-categories-count']}
onChangeDatasetSelection={setCategoryQueryDatasets}
onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration}
selectedDatasets={categoryQueryDatasets}
sourceId={sourceId}
timeRange={categoryQueryTimeRange.timeRange}
topCategories={topLogEntryCategories}
<ViewLogInContext.Provider
sourceId={sourceId}
startTimestamp={categoryQueryTimeRange.timeRange.startTime}
endTimestamp={categoryQueryTimeRange.timeRange.endTime}
>
<ResultsContentPage>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem />
<EuiFlexItem grow={false}>
<EuiSuperDatePicker
start={selectedTimeRange.startTime}
end={selectedTimeRange.endTime}
onTimeChange={handleSelectedTimeRangeChange}
isPaused={autoRefresh.isPaused}
refreshInterval={autoRefresh.interval}
onRefreshChange={handleAutoRefreshChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CategoryJobNoticesSection
hasOutdatedJobConfigurations={hasOutdatedJobConfigurations}
hasOutdatedJobDefinitions={hasOutdatedJobDefinitions}
hasStoppedJobs={hasStoppedJobs}
isFirstUse={isFirstUse}
onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration}
onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate}
qualityWarnings={categoryQualityWarnings}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</ResultsContentPage>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="m">
<TopCategoriesSection
availableDatasets={logEntryCategoryDatasets}
isLoadingDatasets={isLoadingLogEntryCategoryDatasets}
isLoadingTopCategories={isLoadingTopLogEntryCategories}
jobId={jobIds['log-entry-categories-count']}
onChangeDatasetSelection={setCategoryQueryDatasets}
onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration}
selectedDatasets={categoryQueryDatasets}
sourceId={sourceId}
timeRange={categoryQueryTimeRange.timeRange}
topCategories={topLogEntryCategories}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</ResultsContentPage>
<PageViewLogInContext />
</ViewLogInContext.Provider>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ export const CategoryDetailsRow: React.FunctionComponent<{
{logEntryCategoryExamples.map((example, exampleIndex) => (
<CategoryExampleMessage
key={exampleIndex}
id={example.id}
dataset={example.dataset}
message={example.message}
timeRange={timeRange}
timestamp={example.timestamp}
tiebreaker={example.tiebreaker}
context={example.context}
/>
))}
</LogEntryExampleMessages>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useMemo } from 'react';
import React, { useMemo, useState, useCallback, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { encode } from 'rison-node';
import moment from 'moment';

import { getFriendlyNameForPartitionId } from '../../../../../../common/log_analysis';
import { LogEntry, LogEntryContext } from '../../../../../../common/http_api';
import { TimeRange } from '../../../../../../common/http_api/shared';
import {
getFriendlyNameForPartitionId,
partitionField,
} from '../../../../../../common/log_analysis';
import { ViewLogInContext } from '../../../../../containers/logs/view_log_in_context';
import {
LogEntryColumn,
LogEntryFieldColumn,
Expand All @@ -15,24 +24,63 @@ import {
LogEntryTimestampColumn,
} from '../../../../../components/logging/log_text_stream';
import { LogColumnConfiguration } from '../../../../../utils/source_configuration';
import { LogEntryContextMenu } from '../../../../../components/logging/log_text_stream/log_entry_context_menu';
import { useLinkProps } from '../../../../../hooks/use_link_props';

export const exampleMessageScale = 'medium' as const;
export const exampleTimestampFormat = 'dateTime' as const;

export const CategoryExampleMessage: React.FunctionComponent<{
id: string;
dataset: string;
message: string;
timeRange: TimeRange;
timestamp: number;
}> = ({ dataset, message, timestamp }) => {
tiebreaker: number;
context: LogEntryContext;
}> = ({ id, dataset, message, timestamp, timeRange, tiebreaker, context }) => {
const [, { setContextEntry }] = useContext(ViewLogInContext.Context);
// the dataset must be encoded for the field column and the empty value must
// be turned into a user-friendly value
const encodedDatasetFieldValue = useMemo(
() => JSON.stringify(getFriendlyNameForPartitionId(dataset)),
[dataset]
);

const [isHovered, setIsHovered] = useState<boolean>(false);
const setHovered = useCallback(() => setIsHovered(true), []);
const setNotHovered = useCallback(() => setIsHovered(false), []);

const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const openMenu = useCallback(() => setIsMenuOpen(true), []);
const closeMenu = useCallback(() => setIsMenuOpen(false), []);

const viewInStreamLinkProps = useLinkProps({
app: 'logs',
pathname: 'stream',
search: {
logPosition: encode({
end: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
position: { tiebreaker, time: timestamp },
start: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
streamLive: false,
}),
flyoutOptions: encode({
surroundingLogsId: id,
}),
logFilter: encode({
expression: `${partitionField}: ${dataset}`,
kind: 'kuery',
}),
},
});

return (
<LogEntryRowWrapper scale={exampleMessageScale}>
<LogEntryRowWrapper
scale={exampleMessageScale}
onMouseEnter={setHovered}
onMouseLeave={setNotHovered}
>
<LogEntryColumn {...columnWidths[timestampColumnId]}>
<LogEntryTimestampColumn format={exampleTimestampFormat} time={timestamp} />
</LogEntryColumn>
Expand Down Expand Up @@ -60,6 +108,38 @@ export const CategoryExampleMessage: React.FunctionComponent<{
wrapMode="none"
/>
</LogEntryColumn>
<LogEntryColumn {...columnWidths[iconColumnId]}>
{isHovered || isMenuOpen ? (
<LogEntryContextMenu
isOpen={isMenuOpen}
onOpen={openMenu}
onClose={closeMenu}
items={[
{
label: i18n.translate('xpack.infra.logs.categoryExample.viewInStreamText', {
defaultMessage: 'View in stream',
}),
onClick: viewInStreamLinkProps.onClick!,
},
{
label: i18n.translate('xpack.infra.logs.categoryExample.viewInContextText', {
defaultMessage: 'View in context',
}),
onClick: () => {
const logEntry: LogEntry = {
id,
context,
cursor: { time: timestamp, tiebreaker },
columns: [],
};

setContextEntry(logEntry);
},
},
]}
/>
) : null}
</LogEntryColumn>
</LogEntryRowWrapper>
);
};
Expand All @@ -68,6 +148,7 @@ const noHighlights: never[] = [];
const timestampColumnId = 'category-example-timestamp-column' as const;
const messageColumnId = 'category-examples-message-column' as const;
const datasetColumnId = 'category-examples-dataset-column' as const;
const iconColumnId = 'category-examples-icon-column' as const;

const columnWidths = {
[timestampColumnId]: {
Expand All @@ -85,7 +166,12 @@ const columnWidths = {
growWeight: 0,
shrinkWeight: 0,
// w_dataset + w_max_anomaly + w_expand - w_padding = 200 px + 160 px + 40 px + 40 px - 8 px
baseWidth: '432px',
baseWidth: '400px',
},
[iconColumnId]: {
growWeight: 0,
shrinkWeight: 0,
baseWidth: '32px',
},
};

Expand Down
Loading