mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
parent
eba00e803d
commit
0fbe1d1ca8
17 changed files with 414 additions and 46 deletions
BIN
docs/logs/images/logs-action-menu.png
Normal file
BIN
docs/logs/images/logs-action-menu.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 531 B |
BIN
docs/logs/images/logs-view-in-context.png
Normal file
BIN
docs/logs/images/logs-view-in-context.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 379 KiB |
|
@ -69,12 +69,19 @@ To highlight a word or phrase in the logs stream, click *Highlights* and enter y
|
|||
[float]
|
||||
[[logs-event-inspector]]
|
||||
=== Inspect a log event
|
||||
To inspect a log event, hover over it, then click the *View details* icon image:logs/images/logs-view-event.png[View event icon] beside the event.
|
||||
This opens the *Log event document details* fly-out that shows the fields associated with the log event.
|
||||
To inspect a log event, hover over it, then click the *View actions for line* icon image:logs/images/logs-action-menu.png[View actions for line icon]. On the menu that opens, select *View details*. This opens the *Log event document details* fly-out that shows the fields associated with the log event.
|
||||
|
||||
To quickly filter the logs stream by one of the field values, in the log event details, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field.
|
||||
This automatically adds a search filter to the logs stream to filter the entries by this field and value.
|
||||
|
||||
[float]
|
||||
[[log-view-in-context]]
|
||||
=== View log line in context
|
||||
To view a certain line in its context (for example, with other log lines from the same file, or the same cloud container), hover over it, then click the *View actions for line* image:logs/images/logs-action-menu.png[View actions for line icon]. On the menu that opens, select *View in context*. This opens the *View log in context* modal, that shows the log line in its context.
|
||||
|
||||
[role="screenshot"]
|
||||
image::logs/images/logs-view-in-context.png[View a log line in context]
|
||||
|
||||
[float]
|
||||
[[view-log-anomalies]]
|
||||
=== View log anomalies
|
||||
|
|
|
@ -78,11 +78,11 @@ export const logEntryRT = rt.type({
|
|||
id: rt.string,
|
||||
cursor: logEntriesCursorRT,
|
||||
columns: rt.array(logColumnRT),
|
||||
context: rt.partial({
|
||||
'log.file.path': rt.string,
|
||||
'host.name': rt.string,
|
||||
'container.id': rt.string,
|
||||
}),
|
||||
context: rt.union([
|
||||
rt.type({}),
|
||||
rt.type({ 'container.id': rt.string }),
|
||||
rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }),
|
||||
]),
|
||||
});
|
||||
|
||||
export type LogMessageConstantPart = rt.TypeOf<typeof logMessageConstantPartRT>;
|
||||
|
|
|
@ -24,29 +24,49 @@ interface LogEntryActionsColumnProps {
|
|||
isMenuOpen: boolean;
|
||||
onOpenMenu: () => void;
|
||||
onCloseMenu: () => void;
|
||||
onViewDetails: () => void;
|
||||
onViewDetails?: () => void;
|
||||
onViewLogInContext?: () => void;
|
||||
}
|
||||
|
||||
const MENU_LABEL = i18n.translate('xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', {
|
||||
defaultMessage: 'View Details',
|
||||
defaultMessage: 'View actions for line',
|
||||
});
|
||||
|
||||
const LOG_DETAILS_LABEL = i18n.translate('xpack.infra.logs.logEntryActionsDetailsButton', {
|
||||
defaultMessage: 'View actions for line',
|
||||
defaultMessage: 'View details',
|
||||
});
|
||||
|
||||
const LOG_VIEW_IN_CONTEXT_LABEL = i18n.translate(
|
||||
'xpack.infra.lobs.logEntryActionsViewInContextButton',
|
||||
{
|
||||
defaultMessage: 'View in context',
|
||||
}
|
||||
);
|
||||
|
||||
export const LogEntryActionsColumn: React.FC<LogEntryActionsColumnProps> = ({
|
||||
isHovered,
|
||||
isMenuOpen,
|
||||
onOpenMenu,
|
||||
onCloseMenu,
|
||||
onViewDetails,
|
||||
onViewLogInContext,
|
||||
}) => {
|
||||
const handleClickViewDetails = useCallback(() => {
|
||||
onCloseMenu();
|
||||
onViewDetails();
|
||||
|
||||
// Function might be `undefined` and the linter doesn't like that.
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
onViewDetails?.();
|
||||
}, [onCloseMenu, onViewDetails]);
|
||||
|
||||
const handleClickViewInContext = useCallback(() => {
|
||||
onCloseMenu();
|
||||
|
||||
// Function might be `undefined` and the linter doesn't like that.
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
onViewLogInContext?.();
|
||||
}, [onCloseMenu, onViewLogInContext]);
|
||||
|
||||
const button = (
|
||||
<ButtonWrapper>
|
||||
<EuiButtonIcon
|
||||
|
@ -72,6 +92,12 @@ export const LogEntryActionsColumn: React.FC<LogEntryActionsColumnProps> = ({
|
|||
</SectionTitle>
|
||||
<SectionLinks>
|
||||
<SectionLink label={LOG_DETAILS_LABEL} onClick={handleClickViewDetails} />
|
||||
{onViewLogInContext !== undefined ? (
|
||||
<SectionLink
|
||||
label={LOG_VIEW_IN_CONTEXT_LABEL}
|
||||
onClick={handleClickViewInContext}
|
||||
/>
|
||||
) : null}
|
||||
</SectionLinks>
|
||||
</Section>
|
||||
</ActionMenu>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { memo, useState, useCallback, useMemo } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { euiStyled } from '../../../../../observability/public';
|
||||
import { isTimestampColumn } from '../../../utils/log_entry';
|
||||
|
@ -32,6 +33,7 @@ interface LogEntryRowProps {
|
|||
isHighlighted: boolean;
|
||||
logEntry: LogEntry;
|
||||
openFlyoutWithItem?: (id: string) => void;
|
||||
openViewLogInContext?: (entry: LogEntry) => void;
|
||||
scale: TextScale;
|
||||
wrap: boolean;
|
||||
}
|
||||
|
@ -46,6 +48,7 @@ export const LogEntryRow = memo(
|
|||
isHighlighted,
|
||||
logEntry,
|
||||
openFlyoutWithItem,
|
||||
openViewLogInContext,
|
||||
scale,
|
||||
wrap,
|
||||
}: LogEntryRowProps) => {
|
||||
|
@ -63,6 +66,16 @@ export const LogEntryRow = memo(
|
|||
logEntry.id,
|
||||
]);
|
||||
|
||||
const handleOpenViewLogInContext = useCallback(() => openViewLogInContext?.(logEntry), [
|
||||
openViewLogInContext,
|
||||
logEntry,
|
||||
]);
|
||||
|
||||
const hasContext = useMemo(() => !isEmpty(logEntry.context), [logEntry]);
|
||||
const hasActionFlyoutWithItem = openFlyoutWithItem !== undefined;
|
||||
const hasActionViewLogInContext = hasContext && openViewLogInContext !== undefined;
|
||||
const hasActionsMenu = hasActionFlyoutWithItem || hasActionViewLogInContext;
|
||||
|
||||
const logEntryColumnsById = useMemo(
|
||||
() =>
|
||||
logEntry.columns.reduce<{
|
||||
|
@ -165,18 +178,23 @@ export const LogEntryRow = memo(
|
|||
);
|
||||
}
|
||||
})}
|
||||
<LogEntryColumn
|
||||
key="logColumn iconLogColumn iconLogColumn:details"
|
||||
{...columnWidths[iconColumnId]}
|
||||
>
|
||||
<LogEntryActionsColumn
|
||||
isHovered={isHovered}
|
||||
isMenuOpen={isMenuOpen}
|
||||
onOpenMenu={openMenu}
|
||||
onCloseMenu={closeMenu}
|
||||
onViewDetails={openFlyout}
|
||||
/>
|
||||
</LogEntryColumn>
|
||||
{hasActionsMenu ? (
|
||||
<LogEntryColumn
|
||||
key="logColumn iconLogColumn iconLogColumn:details"
|
||||
{...columnWidths[iconColumnId]}
|
||||
>
|
||||
<LogEntryActionsColumn
|
||||
isHovered={isHovered}
|
||||
isMenuOpen={isMenuOpen}
|
||||
onOpenMenu={openMenu}
|
||||
onCloseMenu={closeMenu}
|
||||
onViewDetails={hasActionFlyoutWithItem ? openFlyout : undefined}
|
||||
onViewLogInContext={
|
||||
hasActionViewLogInContext ? handleOpenViewLogInContext : undefined
|
||||
}
|
||||
/>
|
||||
</LogEntryColumn>
|
||||
) : null}
|
||||
</LogEntryRowWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import { MeasurableItemView } from './measurable_item_view';
|
|||
import { VerticalScrollPanel } from './vertical_scroll_panel';
|
||||
import { useColumnWidths, LogEntryColumnWidths } from './log_entry_column';
|
||||
import { LogDateRow } from './log_date_row';
|
||||
import { LogEntry } from '../../../../common/http_api';
|
||||
|
||||
interface ScrollableLogTextStreamViewProps {
|
||||
columnConfigurations: LogColumnConfiguration[];
|
||||
|
@ -50,8 +51,9 @@ interface ScrollableLogTextStreamViewProps {
|
|||
}) => any;
|
||||
loadNewerItems: () => void;
|
||||
reloadItems: () => void;
|
||||
setFlyoutItem: (id: string) => void;
|
||||
setFlyoutVisibility: (visible: boolean) => void;
|
||||
setFlyoutItem?: (id: string) => void;
|
||||
setFlyoutVisibility?: (visible: boolean) => void;
|
||||
setContextEntry?: (entry: LogEntry) => void;
|
||||
highlightedItem: string | null;
|
||||
currentHighlightKey: UniqueTimeKey | null;
|
||||
startDateExpression: string;
|
||||
|
@ -140,9 +142,16 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
|
|||
lastLoadedTime,
|
||||
updateDateRange,
|
||||
startLiveStreaming,
|
||||
setFlyoutItem,
|
||||
setFlyoutVisibility,
|
||||
setContextEntry,
|
||||
} = this.props;
|
||||
|
||||
const { targetId, items, isScrollLocked } = this.state;
|
||||
const hasItems = items.length > 0;
|
||||
const hasFlyoutAction = !!(setFlyoutItem && setFlyoutVisibility);
|
||||
const hasContextAction = !!setContextEntry;
|
||||
|
||||
return (
|
||||
<ScrollableLogTextStreamViewWrapper>
|
||||
{isReloading && (!isStreaming || !hasItems) ? (
|
||||
|
@ -227,7 +236,14 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
|
|||
<LogEntryRow
|
||||
columnConfigurations={columnConfigurations}
|
||||
columnWidths={columnWidths}
|
||||
openFlyoutWithItem={this.handleOpenFlyout}
|
||||
openFlyoutWithItem={
|
||||
hasFlyoutAction ? this.handleOpenFlyout : undefined
|
||||
}
|
||||
openViewLogInContext={
|
||||
hasContextAction
|
||||
? this.handleOpenViewLogInContext
|
||||
: undefined
|
||||
}
|
||||
boundingBoxRef={itemMeasureRef}
|
||||
logEntry={item.logEntry}
|
||||
highlights={item.highlights}
|
||||
|
@ -287,8 +303,19 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
|
|||
}
|
||||
|
||||
private handleOpenFlyout = (id: string) => {
|
||||
this.props.setFlyoutItem(id);
|
||||
this.props.setFlyoutVisibility(true);
|
||||
const { setFlyoutItem, setFlyoutVisibility } = this.props;
|
||||
|
||||
if (setFlyoutItem && setFlyoutVisibility) {
|
||||
setFlyoutItem(id);
|
||||
setFlyoutVisibility(true);
|
||||
}
|
||||
};
|
||||
|
||||
private handleOpenViewLogInContext = (entry: LogEntry) => {
|
||||
const { setContextEntry } = this.props;
|
||||
if (setContextEntry) {
|
||||
setContextEntry(entry);
|
||||
}
|
||||
};
|
||||
|
||||
private handleReload = () => {
|
||||
|
|
|
@ -33,7 +33,7 @@ export const hoveredContentStyle = css`
|
|||
`;
|
||||
|
||||
export const highlightedContentStyle = css`
|
||||
background-color: ${props => props.theme.eui.euiFocusBackgroundColor};
|
||||
background-color: ${props => props.theme.eui.euiColorHighlight};
|
||||
`;
|
||||
|
||||
export const longWrappedContentStyle = css`
|
||||
|
|
|
@ -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 './view_log_in_context';
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { useState, useEffect, useCallback } from 'react';
|
||||
import createContainer from 'constate';
|
||||
import { LogEntry } from '../../../../common/http_api';
|
||||
import { fetchLogEntries } from '../log_entries/api/fetch_log_entries';
|
||||
import { esKuery } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
function getQueryFromLogEntry(entry: LogEntry) {
|
||||
const expression = Object.entries(entry.context).reduce((kuery, [key, value]) => {
|
||||
const currentExpression = `${key} : "${value}"`;
|
||||
if (kuery.length > 0) {
|
||||
return `${kuery} AND ${currentExpression}`;
|
||||
} else {
|
||||
return currentExpression;
|
||||
}
|
||||
}, '');
|
||||
|
||||
return JSON.stringify(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(expression)));
|
||||
}
|
||||
|
||||
interface ViewLogInContextProps {
|
||||
sourceId: string;
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
}
|
||||
|
||||
export interface ViewLogInContextState {
|
||||
entries: LogEntry[];
|
||||
isLoading: boolean;
|
||||
contextEntry?: LogEntry;
|
||||
}
|
||||
|
||||
interface ViewLogInContextCallbacks {
|
||||
setContextEntry: (entry?: LogEntry) => void;
|
||||
}
|
||||
|
||||
export const useViewLogInContext = (
|
||||
props: ViewLogInContextProps
|
||||
): [ViewLogInContextState, ViewLogInContextCallbacks] => {
|
||||
const [contextEntry, setContextEntry] = useState<LogEntry | undefined>();
|
||||
const [entries, setEntries] = useState<LogEntry[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { startTimestamp, endTimestamp, sourceId } = props;
|
||||
|
||||
const maybeFetchLogs = useCallback(async () => {
|
||||
if (contextEntry) {
|
||||
setIsLoading(true);
|
||||
const { data } = await fetchLogEntries({
|
||||
sourceId,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
center: contextEntry.cursor,
|
||||
query: getQueryFromLogEntry(contextEntry),
|
||||
});
|
||||
setEntries(data.entries);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setEntries([]);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [contextEntry, startTimestamp, endTimestamp, sourceId]);
|
||||
|
||||
useEffect(() => {
|
||||
maybeFetchLogs();
|
||||
}, [maybeFetchLogs]);
|
||||
|
||||
return [
|
||||
{
|
||||
contextEntry,
|
||||
entries,
|
||||
isLoading,
|
||||
},
|
||||
{
|
||||
setContextEntry,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const ViewLogInContext = createContainer(useViewLogInContext);
|
|
@ -10,6 +10,7 @@ import { ColumnarPage } from '../../../components/page';
|
|||
import { StreamPageContent } from './page_content';
|
||||
import { StreamPageHeader } from './page_header';
|
||||
import { LogsPageProviders } from './page_providers';
|
||||
import { PageViewLogInContext } from './page_view_log_in_context';
|
||||
import { useTrackPageview } from '../../../../../observability/public';
|
||||
|
||||
export const StreamPage = () => {
|
||||
|
@ -21,6 +22,7 @@ export const StreamPage = () => {
|
|||
<StreamPageHeader />
|
||||
<StreamPageContent />
|
||||
</ColumnarPage>
|
||||
<PageViewLogInContext />
|
||||
</LogsPageProviders>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ import { Source } from '../../../containers/source';
|
|||
|
||||
import { LogsToolbar } from './page_toolbar';
|
||||
import { LogHighlightsState } from '../../../containers/logs/log_highlights';
|
||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||
|
||||
export const LogsPageLogsContent: React.FunctionComponent = () => {
|
||||
const { source, sourceId, version } = useContext(Source.Context);
|
||||
|
@ -55,6 +56,9 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
endDateExpression,
|
||||
updateDateRange,
|
||||
} = useContext(LogPositionState.Context);
|
||||
|
||||
const [, { setContextEntry }] = useContext(ViewLogInContext.Context);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WithLogTextviewUrlState />
|
||||
|
@ -104,6 +108,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
wrap={textWrap}
|
||||
setFlyoutItem={setFlyoutId}
|
||||
setFlyoutVisibility={setFlyoutVisibility}
|
||||
setContextEntry={setContextEntry}
|
||||
highlightedItem={surroundingLogsId ? surroundingLogsId : null}
|
||||
currentHighlightKey={currentHighlightKey}
|
||||
startDateExpression={startDateExpression}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { LogFilterState, WithLogFilterUrlState } from '../../../containers/logs/
|
|||
import { LogEntriesState } from '../../../containers/logs/log_entries';
|
||||
|
||||
import { Source } from '../../../containers/source';
|
||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||
|
||||
const LogFilterStateProvider: React.FC = ({ children }) => {
|
||||
const { createDerivedIndexPattern } = useContext(Source.Context);
|
||||
|
@ -26,6 +27,25 @@ const LogFilterStateProvider: React.FC = ({ children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const ViewLogInContextProvider: React.FC = ({ children }) => {
|
||||
const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context);
|
||||
const { sourceId } = useContext(Source.Context);
|
||||
|
||||
if (!startTimestamp || !endTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewLogInContext.Provider
|
||||
startTimestamp={startTimestamp}
|
||||
endTimestamp={endTimestamp}
|
||||
sourceId={sourceId}
|
||||
>
|
||||
{children}
|
||||
</ViewLogInContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const LogEntriesStateProvider: React.FC = ({ children }) => {
|
||||
const { sourceId } = useContext(Source.Context);
|
||||
const {
|
||||
|
@ -91,11 +111,13 @@ export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
|
|||
<LogFlyout.Provider>
|
||||
<LogPositionState.Provider>
|
||||
<WithLogPositionUrlState />
|
||||
<LogFilterStateProvider>
|
||||
<LogEntriesStateProvider>
|
||||
<LogHighlightsStateProvider>{children}</LogHighlightsStateProvider>
|
||||
</LogEntriesStateProvider>
|
||||
</LogFilterStateProvider>
|
||||
<ViewLogInContextProvider>
|
||||
<LogFilterStateProvider>
|
||||
<LogEntriesStateProvider>
|
||||
<LogHighlightsStateProvider>{children}</LogHighlightsStateProvider>
|
||||
</LogEntriesStateProvider>
|
||||
</LogFilterStateProvider>
|
||||
</ViewLogInContextProvider>
|
||||
</LogPositionState.Provider>
|
||||
</LogFlyout.Provider>
|
||||
</LogViewConfiguration.Provider>
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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, useCallback, useMemo } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import {
|
||||
EuiOverlayMask,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { ViewLogInContext } from '../../../containers/logs/view_log_in_context';
|
||||
import { LogEntry } from '../../../../common/http_api';
|
||||
import { Source } from '../../../containers/source';
|
||||
import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration';
|
||||
import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream';
|
||||
import { useViewportDimensions } from '../../../utils/use_viewport_dimensions';
|
||||
|
||||
const MODAL_MARGIN = 25;
|
||||
|
||||
export const PageViewLogInContext: React.FC = () => {
|
||||
const { source } = useContext(Source.Context);
|
||||
const { textScale, textWrap } = useContext(LogViewConfiguration.Context);
|
||||
const columnConfigurations = useMemo(() => (source && source.configuration.logColumns) || [], [
|
||||
source,
|
||||
]);
|
||||
const [{ contextEntry, entries, isLoading }, { setContextEntry }] = useContext(
|
||||
ViewLogInContext.Context
|
||||
);
|
||||
const closeModal = useCallback(() => setContextEntry(undefined), [setContextEntry]);
|
||||
const { width: vw, height: vh } = useViewportDimensions();
|
||||
|
||||
const streamItems = useMemo(
|
||||
() =>
|
||||
entries.map(entry => ({
|
||||
kind: 'logEntry' as const,
|
||||
logEntry: entry,
|
||||
highlights: [],
|
||||
})),
|
||||
[entries]
|
||||
);
|
||||
|
||||
if (!contextEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={closeModal} maxWidth={false}>
|
||||
<EuiModalBody style={{ width: vw - MODAL_MARGIN * 2, height: vh - MODAL_MARGIN * 2 }}>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
responsive={false}
|
||||
wrap={false}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<EuiFlexItem grow={1}>
|
||||
<LogEntryContext context={contextEntry.context} />
|
||||
<ScrollableLogTextStreamView
|
||||
target={contextEntry.cursor}
|
||||
columnConfigurations={columnConfigurations}
|
||||
items={streamItems}
|
||||
scale={textScale}
|
||||
wrap={textWrap}
|
||||
isReloading={isLoading}
|
||||
isLoadingMore={false}
|
||||
hasMoreBeforeStart={false}
|
||||
hasMoreAfterEnd={false}
|
||||
isStreaming={false}
|
||||
lastLoadedTime={null}
|
||||
jumpToTarget={noop}
|
||||
reportVisibleInterval={noop}
|
||||
loadNewerItems={noop}
|
||||
reloadItems={noop}
|
||||
highlightedItem={contextEntry.id}
|
||||
currentHighlightKey={null}
|
||||
startDateExpression={''}
|
||||
endDateExpression={''}
|
||||
updateDateRange={noop}
|
||||
startLiveStreaming={noop}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiModalBody>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
||||
|
||||
const LogEntryContext: React.FC<{ context: LogEntry['context'] }> = ({ context }) => {
|
||||
if ('container.id' in context) {
|
||||
return <p>Displayed logs are from container {context['container.id']}</p>;
|
||||
}
|
||||
|
||||
if ('host.name' in context) {
|
||||
const shortenedFilePath =
|
||||
context['log.file.path'].length > 45
|
||||
? context['log.file.path'].slice(0, 20) + '...' + context['log.file.path'].slice(-25)
|
||||
: context['log.file.path'];
|
||||
|
||||
return (
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<EuiTextColor color="subdued">
|
||||
Displayed logs are from file{' '}
|
||||
<EuiToolTip content={context['log.file.path']}>
|
||||
<span>{shortenedFilePath}</span>
|
||||
</EuiToolTip>{' '}
|
||||
and host {context['host.name']}
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
39
x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts
Normal file
39
x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { useState, useEffect } from 'react';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
interface ViewportDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const getViewportWidth = () =>
|
||||
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
const getViewportHeight = () =>
|
||||
window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
|
||||
|
||||
export function useViewportDimensions(): ViewportDimensions {
|
||||
const [dimensions, setDimensions] = useState<ViewportDimensions>({
|
||||
width: getViewportWidth(),
|
||||
height: getViewportHeight(),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = throttle(() => {
|
||||
setDimensions({
|
||||
width: getViewportWidth(),
|
||||
height: getViewportHeight(),
|
||||
});
|
||||
}, 250);
|
||||
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
return () => window.removeEventListener('resize', updateDimensions);
|
||||
}, []);
|
||||
|
||||
return dimensions;
|
||||
}
|
|
@ -156,14 +156,7 @@ export class InfraLogEntriesDomain {
|
|||
}
|
||||
}
|
||||
),
|
||||
context: FIELDS_FROM_CONTEXT.reduce<LogEntry['context']>((ctx, field) => {
|
||||
// Users might have different types here in their mappings.
|
||||
const value = doc.fields[field];
|
||||
if (typeof value === 'string') {
|
||||
ctx[field] = value;
|
||||
}
|
||||
return ctx;
|
||||
}, {}),
|
||||
context: getContextFromDoc(doc),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -352,3 +345,20 @@ const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({
|
|||
type: 'phrase',
|
||||
},
|
||||
});
|
||||
|
||||
const getContextFromDoc = (doc: LogEntryDocument): LogEntry['context'] => {
|
||||
// Get all context fields, then test for the presence and type of the ones that go together
|
||||
const containerId = doc.fields['container.id'];
|
||||
const hostName = doc.fields['host.name'];
|
||||
const logFilePath = doc.fields['log.file.path'];
|
||||
|
||||
if (typeof containerId === 'string') {
|
||||
return { 'container.id': containerId };
|
||||
}
|
||||
|
||||
if (typeof hostName === 'string' && typeof logFilePath === 'string') {
|
||||
return { 'host.name': hostName, 'log.file.path': logFilePath };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
|
|
@ -126,7 +126,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
expect(messageColumn.message.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('Returns the context fields', async () => {
|
||||
it('Does not build context if entry does not have all fields', async () => {
|
||||
const { body } = await supertest
|
||||
.post(LOG_ENTRIES_PATH)
|
||||
.set(COMMON_HEADERS)
|
||||
|
@ -147,9 +147,7 @@ export default function({ getService }: FtrProviderContext) {
|
|||
|
||||
const entries = logEntriesResponse.data.entries;
|
||||
const entry = entries[0];
|
||||
|
||||
expect(entry.context).to.have.property('host.name');
|
||||
expect(entry.context['host.name']).to.be('demo-stack-nginx-01');
|
||||
expect(entry.context).to.eql({});
|
||||
});
|
||||
|
||||
it('Paginates correctly with `after`', async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue