mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Backports the following commits to 7.x: - [Logs UI] Add single phrase highlighting (#39569)
This commit is contained in:
parent
2f63d5fed5
commit
f68b5a1edb
54 changed files with 1551 additions and 286 deletions
|
@ -30,9 +30,11 @@ export const sharedFragments = {
|
|||
}
|
||||
columns {
|
||||
... on InfraLogEntryTimestampColumn {
|
||||
columnId
|
||||
timestamp
|
||||
}
|
||||
... on InfraLogEntryMessageColumn {
|
||||
columnId
|
||||
message {
|
||||
... on InfraLogMessageFieldSegment {
|
||||
field
|
||||
|
@ -44,10 +46,36 @@ export const sharedFragments = {
|
|||
}
|
||||
}
|
||||
... on InfraLogEntryFieldColumn {
|
||||
columnId
|
||||
field
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
InfraLogEntryHighlightFields: gql`
|
||||
fragment InfraLogEntryHighlightFields on InfraLogEntry {
|
||||
gid
|
||||
key {
|
||||
time
|
||||
tiebreaker
|
||||
}
|
||||
columns {
|
||||
... on InfraLogEntryMessageColumn {
|
||||
columnId
|
||||
message {
|
||||
... on InfraLogMessageFieldSegment {
|
||||
field
|
||||
highlights
|
||||
}
|
||||
}
|
||||
}
|
||||
... on InfraLogEntryFieldColumn {
|
||||
columnId
|
||||
field
|
||||
highlights
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -34,6 +34,8 @@ export interface InfraSource {
|
|||
logEntriesAround: InfraLogEntryInterval;
|
||||
/** A consecutive span of log entries within an interval */
|
||||
logEntriesBetween: InfraLogEntryInterval;
|
||||
/** Sequences of log entries matching sets of highlighting queries within an interval */
|
||||
logEntryHighlights: InfraLogEntryInterval[];
|
||||
/** A consecutive span of summary buckets within an interval */
|
||||
logSummaryBetween: InfraLogSummaryInterval;
|
||||
|
||||
|
@ -181,11 +183,15 @@ export interface InfraLogEntry {
|
|||
}
|
||||
/** A special built-in column that contains the log entry's timestamp */
|
||||
export interface InfraLogEntryTimestampColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** The timestamp */
|
||||
timestamp: number;
|
||||
}
|
||||
/** A special built-in column that contains the log entry's constructed message */
|
||||
export interface InfraLogEntryMessageColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** A list of the formatted log entry segments */
|
||||
message: InfraLogMessageSegment[];
|
||||
}
|
||||
|
@ -205,10 +211,14 @@ export interface InfraLogMessageConstantSegment {
|
|||
}
|
||||
/** A column that contains the value of a field of the log entry */
|
||||
export interface InfraLogEntryFieldColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** The field name of the column */
|
||||
field: string;
|
||||
/** The value of the field in the log entry */
|
||||
value: string;
|
||||
/** A list of highlighted substrings of the value */
|
||||
highlights: string[];
|
||||
}
|
||||
/** A consecutive sequence of log summary buckets */
|
||||
export interface InfraLogSummaryInterval {
|
||||
|
@ -325,6 +335,10 @@ export interface InfraTimeKeyInput {
|
|||
tiebreaker: number;
|
||||
}
|
||||
|
||||
export interface InfraLogEntryHighlightInput {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface InfraTimerangeInput {
|
||||
/** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
|
||||
interval: string;
|
||||
|
@ -419,8 +433,6 @@ export interface LogEntriesAroundInfraSourceArgs {
|
|||
countAfter?: number | null;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The query to highlight the log entries with */
|
||||
highlightQuery?: string | null;
|
||||
}
|
||||
export interface LogEntriesBetweenInfraSourceArgs {
|
||||
/** The sort key that corresponds to the start of the interval */
|
||||
|
@ -429,8 +441,16 @@ export interface LogEntriesBetweenInfraSourceArgs {
|
|||
endKey: InfraTimeKeyInput;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The query to highlight the log entries with */
|
||||
highlightQuery?: string | null;
|
||||
}
|
||||
export interface LogEntryHighlightsInfraSourceArgs {
|
||||
/** The sort key that corresponds to the start of the interval */
|
||||
startKey: InfraTimeKeyInput;
|
||||
/** The sort key that corresponds to the end of the interval */
|
||||
endKey: InfraTimeKeyInput;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The highlighting to apply to the log entries */
|
||||
highlights: InfraLogEntryHighlightInput[];
|
||||
}
|
||||
export interface LogSummaryBetweenInfraSourceArgs {
|
||||
/** The millisecond timestamp that corresponds to the start of the interval */
|
||||
|
@ -612,6 +632,46 @@ export namespace FlyoutItemQuery {
|
|||
};
|
||||
}
|
||||
|
||||
export namespace LogEntryHighlightsQuery {
|
||||
export type Variables = {
|
||||
sourceId?: string | null;
|
||||
startKey: InfraTimeKeyInput;
|
||||
endKey: InfraTimeKeyInput;
|
||||
filterQuery?: string | null;
|
||||
highlights: InfraLogEntryHighlightInput[];
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
|
||||
id: string;
|
||||
|
||||
logEntryHighlights: LogEntryHighlights[];
|
||||
};
|
||||
|
||||
export type LogEntryHighlights = {
|
||||
__typename?: 'InfraLogEntryInterval';
|
||||
|
||||
start?: Start | null;
|
||||
|
||||
end?: End | null;
|
||||
|
||||
entries: Entries[];
|
||||
};
|
||||
|
||||
export type Start = InfraTimeKeyFields.Fragment;
|
||||
|
||||
export type End = InfraTimeKeyFields.Fragment;
|
||||
|
||||
export type Entries = InfraLogEntryHighlightFields.Fragment;
|
||||
}
|
||||
|
||||
export namespace LogSummary {
|
||||
export type Variables = {
|
||||
sourceId?: string | null;
|
||||
|
@ -1085,12 +1145,16 @@ export namespace InfraLogEntryFields {
|
|||
export type InfraLogEntryTimestampColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryTimestampColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type InfraLogEntryMessageColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryMessageColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
message: Message[];
|
||||
};
|
||||
|
||||
|
@ -1115,8 +1179,62 @@ export namespace InfraLogEntryFields {
|
|||
export type InfraLogEntryFieldColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryFieldColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
field: string;
|
||||
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace InfraLogEntryHighlightFields {
|
||||
export type Fragment = {
|
||||
__typename?: 'InfraLogEntry';
|
||||
|
||||
gid: string;
|
||||
|
||||
key: Key;
|
||||
|
||||
columns: Columns[];
|
||||
};
|
||||
|
||||
export type Key = {
|
||||
__typename?: 'InfraTimeKey';
|
||||
|
||||
time: number;
|
||||
|
||||
tiebreaker: number;
|
||||
};
|
||||
|
||||
export type Columns =
|
||||
| InfraLogEntryMessageColumnInlineFragment
|
||||
| InfraLogEntryFieldColumnInlineFragment;
|
||||
|
||||
export type InfraLogEntryMessageColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryMessageColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
message: Message[];
|
||||
};
|
||||
|
||||
export type Message = InfraLogMessageFieldSegmentInlineFragment;
|
||||
|
||||
export type InfraLogMessageFieldSegmentInlineFragment = {
|
||||
__typename?: 'InfraLogMessageFieldSegment';
|
||||
|
||||
field: string;
|
||||
|
||||
highlights: string[];
|
||||
};
|
||||
|
||||
export type InfraLogEntryFieldColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryFieldColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
field: string;
|
||||
|
||||
highlights: string[];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -77,3 +77,15 @@ export const getIndexAtTimeKey = <Value>(
|
|||
|
||||
export const timeKeyIsBetween = (min: TimeKey, max: TimeKey, operand: TimeKey) =>
|
||||
compareTimeKeys(min, operand) <= 0 && compareTimeKeys(max, operand) >= 0;
|
||||
|
||||
export const getPreviousTimeKey = (timeKey: TimeKey) => ({
|
||||
...timeKey,
|
||||
time: timeKey.time,
|
||||
tiebreaker: timeKey.tiebreaker - 1,
|
||||
});
|
||||
|
||||
export const getNextTimeKey = (timeKey: TimeKey) => ({
|
||||
...timeKey,
|
||||
time: timeKey.time,
|
||||
tiebreaker: timeKey.tiebreaker + 1,
|
||||
});
|
||||
|
|
|
@ -6,18 +6,19 @@
|
|||
|
||||
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import url from 'url';
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { InfraLogItem } from '../../../graphql/types';
|
||||
import { useVisibilityState } from '../../../utils/use_visibility_state';
|
||||
|
||||
const UPTIME_FIELDS = ['container.id', 'host.ip', 'kubernetes.pod.uid'];
|
||||
|
||||
export const LogEntryActionsMenu: React.FunctionComponent<{
|
||||
logItem: InfraLogItem;
|
||||
}> = ({ logItem }) => {
|
||||
const { hide, isVisible, show } = useVisibility();
|
||||
const { hide, isVisible, show } = useVisibilityState(false);
|
||||
|
||||
const uptimeLink = useMemo(() => getUptimeLink(logItem), [logItem]);
|
||||
|
||||
|
@ -82,15 +83,6 @@ export const LogEntryActionsMenu: React.FunctionComponent<{
|
|||
);
|
||||
};
|
||||
|
||||
const useVisibility = (initialVisibility: boolean = false) => {
|
||||
const [isVisible, setIsVisible] = useState(initialVisibility);
|
||||
|
||||
const hide = useCallback(() => setIsVisible(false), [setIsVisible]);
|
||||
const show = useCallback(() => setIsVisible(true), [setIsVisible]);
|
||||
|
||||
return { hide, isVisible, show };
|
||||
};
|
||||
|
||||
const getUptimeLink = (logItem: InfraLogItem) => {
|
||||
const searchExpressions = logItem.fields
|
||||
.filter(({ field, value }) => value != null && UPTIME_FIELDS.includes(field))
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiButtonIcon,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { debounce } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import euiStyled from '../../../../../common/eui_styled_components';
|
||||
import { useVisibilityState } from '../../utils/use_visibility_state';
|
||||
|
||||
interface LogHighlightsMenuProps {
|
||||
onChange: (highlightTerms: string[]) => void;
|
||||
isLoading: boolean;
|
||||
activeHighlights: boolean;
|
||||
}
|
||||
|
||||
export const LogHighlightsMenu: React.FC<LogHighlightsMenuProps> = ({
|
||||
onChange,
|
||||
isLoading,
|
||||
activeHighlights,
|
||||
}) => {
|
||||
const {
|
||||
isVisible: isPopoverOpen,
|
||||
hide: closePopover,
|
||||
toggle: togglePopover,
|
||||
} = useVisibilityState(false);
|
||||
|
||||
// Input field state
|
||||
const [highlightTerm, setHighlightTerm] = useState('');
|
||||
const debouncedOnChange = useMemo(() => debounce(onChange, 275), [onChange]);
|
||||
const changeHighlightTerm = useCallback(
|
||||
e => {
|
||||
const value = e.target.value;
|
||||
setHighlightTerm(value);
|
||||
},
|
||||
[setHighlightTerm]
|
||||
);
|
||||
const clearHighlightTerm = useCallback(() => setHighlightTerm(''), [setHighlightTerm]);
|
||||
useEffect(
|
||||
() => {
|
||||
debouncedOnChange([highlightTerm]);
|
||||
},
|
||||
[highlightTerm]
|
||||
);
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty color="text" size="xs" iconType="brush" onClick={togglePopover}>
|
||||
<FormattedMessage
|
||||
id="xpack.infra.logs.highlights.highlightsPopoverButtonLabel"
|
||||
defaultMessage="Highlights"
|
||||
/>
|
||||
{activeHighlights ? <ActiveHighlightsIndicator /> : null}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="popover"
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
ownFocus
|
||||
>
|
||||
<LogHighlightsMenuContent>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldText
|
||||
placeholder={termsFieldLabel}
|
||||
fullWidth={true}
|
||||
value={highlightTerm}
|
||||
onChange={changeHighlightTerm}
|
||||
isLoading={isLoading}
|
||||
aria-label={termsFieldLabel}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
aria-label={clearTermsButtonLabel}
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={clearHighlightTerm}
|
||||
title={clearTermsButtonLabel}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</LogHighlightsMenuContent>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
const termsFieldLabel = i18n.translate('xpack.infra.logs.highlights.highlightTermsFieldLabel', {
|
||||
defaultMessage: 'Terms to highlight',
|
||||
});
|
||||
|
||||
const clearTermsButtonLabel = i18n.translate(
|
||||
'xpack.infra.logs.highlights.clearHighlightTermsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Clear terms to highlight',
|
||||
}
|
||||
);
|
||||
|
||||
const ActiveHighlightsIndicator = euiStyled(EuiIcon).attrs({
|
||||
type: 'checkInCircleFilled',
|
||||
size: 'm',
|
||||
color: props => props.theme.eui.euiColorAccent,
|
||||
})`
|
||||
padding-left: ${props => props.theme.eui.paddingSizes.xs};
|
||||
`;
|
||||
|
||||
const LogHighlightsMenuContent = euiStyled.div`
|
||||
width: 300px;
|
||||
`;
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import euiStyled from '../../../../../../common/eui_styled_components';
|
||||
import {
|
||||
|
@ -15,16 +15,20 @@ import {
|
|||
isFieldLogColumnConfiguration,
|
||||
isMessageLogColumnConfiguration,
|
||||
} from '../../../utils/source_configuration';
|
||||
import { LogEntryColumnWidth, LogEntryColumn, LogEntryColumnContent } from './log_entry_column';
|
||||
import {
|
||||
LogEntryColumn,
|
||||
LogEntryColumnContent,
|
||||
LogEntryColumnWidth,
|
||||
LogEntryColumnWidths,
|
||||
iconColumnId,
|
||||
} from './log_entry_column';
|
||||
import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel';
|
||||
|
||||
export const LogColumnHeaders = injectI18n<{
|
||||
columnConfigurations: LogColumnConfiguration[];
|
||||
columnWidths: LogEntryColumnWidth[];
|
||||
columnWidths: LogEntryColumnWidths;
|
||||
showColumnConfiguration: () => void;
|
||||
}>(({ columnConfigurations, columnWidths, intl, showColumnConfiguration }) => {
|
||||
const iconColumnWidth = useMemo(() => columnWidths[columnWidths.length - 1], [columnWidths]);
|
||||
|
||||
const showColumnConfigurationLabel = intl.formatMessage({
|
||||
id: 'xpack.infra.logColumnHeaders.configureColumnsLabel',
|
||||
defaultMessage: 'Configure columns',
|
||||
|
@ -32,12 +36,11 @@ export const LogColumnHeaders = injectI18n<{
|
|||
|
||||
return (
|
||||
<LogColumnHeadersWrapper>
|
||||
{columnConfigurations.map((columnConfiguration, columnIndex) => {
|
||||
const columnWidth = columnWidths[columnIndex];
|
||||
{columnConfigurations.map(columnConfiguration => {
|
||||
if (isTimestampLogColumnConfiguration(columnConfiguration)) {
|
||||
return (
|
||||
<LogColumnHeader
|
||||
columnWidth={columnWidth}
|
||||
columnWidth={columnWidths[columnConfiguration.timestampColumn.id]}
|
||||
data-test-subj="logColumnHeader timestampLogColumnHeader"
|
||||
key={columnConfiguration.timestampColumn.id}
|
||||
>
|
||||
|
@ -47,7 +50,7 @@ export const LogColumnHeaders = injectI18n<{
|
|||
} else if (isMessageLogColumnConfiguration(columnConfiguration)) {
|
||||
return (
|
||||
<LogColumnHeader
|
||||
columnWidth={columnWidth}
|
||||
columnWidth={columnWidths[columnConfiguration.messageColumn.id]}
|
||||
data-test-subj="logColumnHeader messageLogColumnHeader"
|
||||
key={columnConfiguration.messageColumn.id}
|
||||
>
|
||||
|
@ -57,7 +60,7 @@ export const LogColumnHeaders = injectI18n<{
|
|||
} else if (isFieldLogColumnConfiguration(columnConfiguration)) {
|
||||
return (
|
||||
<LogColumnHeader
|
||||
columnWidth={columnWidth}
|
||||
columnWidth={columnWidths[columnConfiguration.fieldColumn.id]}
|
||||
data-test-subj="logColumnHeader fieldLogColumnHeader"
|
||||
key={columnConfiguration.fieldColumn.id}
|
||||
>
|
||||
|
@ -67,7 +70,7 @@ export const LogColumnHeaders = injectI18n<{
|
|||
}
|
||||
})}
|
||||
<LogColumnHeader
|
||||
columnWidth={iconColumnWidth}
|
||||
columnWidth={columnWidths[iconColumnId]}
|
||||
data-test-subj="logColumnHeader iconLogColumnHeader"
|
||||
key="iconColumnHeader"
|
||||
>
|
||||
|
|
|
@ -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 React from 'react';
|
||||
|
||||
import euiStyled from '../../../../../../common/eui_styled_components';
|
||||
import { tintOrShade } from '../../../utils/styles';
|
||||
|
||||
export const HighlightMarker = euiStyled.mark`
|
||||
background-color: ${props =>
|
||||
tintOrShade(props.theme.eui.euiTextColor, props.theme.eui.euiColorAccent, 0.7, 0.5)};
|
||||
`;
|
||||
|
||||
export const highlightFieldValue = (
|
||||
value: string,
|
||||
highlightTerms: string[],
|
||||
HighlightComponent: React.ComponentType
|
||||
) =>
|
||||
highlightTerms.reduce<React.ReactNode[]>(
|
||||
(fragments, highlightTerm, index) => {
|
||||
const lastFragment = fragments[fragments.length - 1];
|
||||
|
||||
if (typeof lastFragment !== 'string') {
|
||||
return fragments;
|
||||
}
|
||||
|
||||
const highlightTermPosition = lastFragment.indexOf(highlightTerm);
|
||||
|
||||
if (highlightTermPosition > -1) {
|
||||
return [
|
||||
...fragments.slice(0, fragments.length - 1),
|
||||
lastFragment.slice(0, highlightTermPosition),
|
||||
<HighlightComponent key={`highlight-${highlightTerm}-${index}`}>
|
||||
{highlightTerm}
|
||||
</HighlightComponent>,
|
||||
lastFragment.slice(highlightTermPosition + highlightTerm.length),
|
||||
];
|
||||
} else {
|
||||
return fragments;
|
||||
}
|
||||
},
|
||||
[value]
|
||||
);
|
|
@ -7,13 +7,14 @@
|
|||
import { bisector } from 'd3-array';
|
||||
|
||||
import { compareToTimeKey, TimeKey } from '../../../../common/time';
|
||||
import { LogEntry } from '../../../utils/log_entry';
|
||||
import { LogEntry, LogEntryHighlight } from '../../../utils/log_entry';
|
||||
|
||||
export type StreamItem = LogEntryStreamItem;
|
||||
|
||||
export interface LogEntryStreamItem {
|
||||
kind: 'logEntry';
|
||||
logEntry: LogEntry;
|
||||
highlights: LogEntryHighlight[];
|
||||
}
|
||||
|
||||
export function getStreamItemTimeKey(item: StreamItem) {
|
||||
|
|
|
@ -44,38 +44,59 @@ export type LogEntryColumnWidth = Pick<
|
|||
'baseWidth' | 'growWeight' | 'shrinkWeight'
|
||||
>;
|
||||
|
||||
export const iconColumnId = Symbol('iconColumnId');
|
||||
|
||||
export interface LogEntryColumnWidths {
|
||||
[columnId: string]: LogEntryColumnWidth;
|
||||
[iconColumnId]: LogEntryColumnWidth;
|
||||
}
|
||||
|
||||
export const getColumnWidths = (
|
||||
columns: LogColumnConfiguration[],
|
||||
characterWidth: number,
|
||||
formattedDateWidth: number
|
||||
): LogEntryColumnWidth[] => [
|
||||
...columns.map(column => {
|
||||
if (isTimestampLogColumnConfiguration(column)) {
|
||||
return {
|
||||
): LogEntryColumnWidths =>
|
||||
columns.reduce<LogEntryColumnWidths>(
|
||||
(columnWidths, column) => {
|
||||
if (isTimestampLogColumnConfiguration(column)) {
|
||||
return {
|
||||
...columnWidths,
|
||||
[column.timestampColumn.id]: {
|
||||
growWeight: 0,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: `${Math.ceil(
|
||||
characterWidth * formattedDateWidth * DATE_COLUMN_SLACK_FACTOR
|
||||
) +
|
||||
2 * COLUMN_PADDING}px`,
|
||||
},
|
||||
};
|
||||
} else if (isMessageLogColumnConfiguration(column)) {
|
||||
return {
|
||||
...columnWidths,
|
||||
[column.messageColumn.id]: {
|
||||
growWeight: 5,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: '0%',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...columnWidths,
|
||||
[column.fieldColumn.id]: {
|
||||
growWeight: 1,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: `${Math.ceil(characterWidth * FIELD_COLUMN_MIN_WIDTH_CHARACTERS) +
|
||||
2 * COLUMN_PADDING}px`,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
// the detail flyout icon column
|
||||
[iconColumnId]: {
|
||||
growWeight: 0,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: `${Math.ceil(characterWidth * formattedDateWidth * DATE_COLUMN_SLACK_FACTOR) +
|
||||
2 * COLUMN_PADDING}px`,
|
||||
};
|
||||
} else if (isMessageLogColumnConfiguration(column)) {
|
||||
return {
|
||||
growWeight: 5,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: '0%',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
growWeight: 1,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: `${Math.ceil(characterWidth * FIELD_COLUMN_MIN_WIDTH_CHARACTERS) +
|
||||
2 * COLUMN_PADDING}px`,
|
||||
};
|
||||
baseWidth: `${DETAIL_FLYOUT_ICON_MIN_WIDTH + 2 * COLUMN_PADDING}px`,
|
||||
},
|
||||
}
|
||||
}),
|
||||
// the detail flyout icon column
|
||||
{
|
||||
growWeight: 0,
|
||||
shrinkWeight: 0,
|
||||
baseWidth: `${DETAIL_FLYOUT_ICON_MIN_WIDTH + 2 * COLUMN_PADDING}px`,
|
||||
},
|
||||
];
|
||||
);
|
||||
|
|
|
@ -4,23 +4,32 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { LogEntryFieldColumn } from './log_entry_field_column';
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { EuiThemeProvider } from '../../../../../../common/eui_styled_components';
|
||||
import { LogEntryColumn } from '../../../utils/log_entry';
|
||||
import { LogEntryFieldColumn } from './log_entry_field_column';
|
||||
|
||||
describe('LogEntryFieldColumn', () => {
|
||||
it('should output a <ul> when displaying an Array of values', () => {
|
||||
const encodedValue = '["a","b","c"]'; // Using JSON.stringify here fails the test when running locally on macOS
|
||||
const column: LogEntryColumn = {
|
||||
columnId: 'TEST_COLUMN',
|
||||
field: 'TEST_FIELD',
|
||||
value: JSON.stringify(['a', 'b', 'c']),
|
||||
};
|
||||
|
||||
const component = mount(
|
||||
<LogEntryFieldColumn
|
||||
encodedValue={encodedValue}
|
||||
columnValue={column}
|
||||
highlights={[]}
|
||||
isHighlighted={false}
|
||||
isHovered={false}
|
||||
isWrapped={false}
|
||||
/>,
|
||||
{ wrappingComponent: EuiThemeProvider } as any // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36075
|
||||
);
|
||||
|
||||
expect(component.exists('ul')).toBe(true);
|
||||
expect(
|
||||
component.containsAllMatchingElements([
|
||||
|
@ -31,16 +40,23 @@ describe('LogEntryFieldColumn', () => {
|
|||
).toBe(true);
|
||||
});
|
||||
it('should output just text when passed a non-Array', () => {
|
||||
const encodedValue = JSON.stringify('foo');
|
||||
const column: LogEntryColumn = {
|
||||
columnId: 'TEST_COLUMN',
|
||||
field: 'TEST_FIELD',
|
||||
value: JSON.stringify('foo'),
|
||||
};
|
||||
|
||||
const component = mount(
|
||||
<LogEntryFieldColumn
|
||||
encodedValue={encodedValue}
|
||||
columnValue={column}
|
||||
highlights={[]}
|
||||
isHighlighted={false}
|
||||
isHovered={false}
|
||||
isWrapped={false}
|
||||
/>,
|
||||
{ wrappingComponent: EuiThemeProvider } as any // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36075
|
||||
);
|
||||
|
||||
expect(component.exists('ul')).toBe(false);
|
||||
expect(component.text()).toEqual('foo');
|
||||
});
|
||||
|
|
|
@ -8,31 +8,53 @@ import { darken, transparentize } from 'polished';
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import styled, { css } from '../../../../../../common/eui_styled_components';
|
||||
import {
|
||||
isFieldColumn,
|
||||
isHighlightFieldColumn,
|
||||
LogEntryColumn,
|
||||
LogEntryHighlightColumn,
|
||||
} from '../../../utils/log_entry';
|
||||
import { highlightFieldValue, HighlightMarker } from './highlighting';
|
||||
import { LogEntryColumnContent } from './log_entry_column';
|
||||
|
||||
interface LogEntryFieldColumnProps {
|
||||
encodedValue: string;
|
||||
columnValue: LogEntryColumn;
|
||||
highlights: LogEntryHighlightColumn[];
|
||||
isHighlighted: boolean;
|
||||
isHovered: boolean;
|
||||
isWrapped: boolean;
|
||||
}
|
||||
|
||||
export const LogEntryFieldColumn: React.FunctionComponent<LogEntryFieldColumnProps> = ({
|
||||
encodedValue,
|
||||
columnValue,
|
||||
highlights: [firstHighlight], // we only support one highlight for now
|
||||
isHighlighted,
|
||||
isHovered,
|
||||
isWrapped,
|
||||
}) => {
|
||||
const value = useMemo(() => JSON.parse(encodedValue), [encodedValue]);
|
||||
const value = useMemo(() => (isFieldColumn(columnValue) ? JSON.parse(columnValue.value) : null), [
|
||||
columnValue,
|
||||
]);
|
||||
const formattedValue = Array.isArray(value) ? (
|
||||
<ul>
|
||||
{value.map((entry, i) => (
|
||||
<CommaSeparatedLi key={`LogEntryFieldColumn-${i}`}>{entry}</CommaSeparatedLi>
|
||||
<CommaSeparatedLi key={`LogEntryFieldColumn-${i}`}>
|
||||
{highlightFieldValue(
|
||||
entry,
|
||||
isHighlightFieldColumn(firstHighlight) ? firstHighlight.highlights : [],
|
||||
HighlightMarker
|
||||
)}
|
||||
</CommaSeparatedLi>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
value
|
||||
highlightFieldValue(
|
||||
value,
|
||||
isHighlightFieldColumn(firstHighlight) ? firstHighlight.highlights : [],
|
||||
HighlightMarker
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<FieldColumnContent isHighlighted={isHighlighted} isHovered={isHovered} isWrapped={isWrapped}>
|
||||
{formattedValue}
|
||||
|
|
|
@ -10,21 +10,33 @@ import { css } from '../../../../../../common/eui_styled_components';
|
|||
import {
|
||||
isConstantSegment,
|
||||
isFieldSegment,
|
||||
isHighlightMessageColumn,
|
||||
isMessageColumn,
|
||||
LogEntryColumn,
|
||||
LogEntryHighlightColumn,
|
||||
LogEntryMessageSegment,
|
||||
} from '../../../utils/log_entry';
|
||||
import { highlightFieldValue, HighlightMarker } from './highlighting';
|
||||
import { LogEntryColumnContent } from './log_entry_column';
|
||||
import { hoveredContentStyle } from './text_styles';
|
||||
|
||||
interface LogEntryMessageColumnProps {
|
||||
segments: LogEntryMessageSegment[];
|
||||
columnValue: LogEntryColumn;
|
||||
highlights: LogEntryHighlightColumn[];
|
||||
isHighlighted: boolean;
|
||||
isHovered: boolean;
|
||||
isWrapped: boolean;
|
||||
isHighlighted: boolean;
|
||||
}
|
||||
|
||||
export const LogEntryMessageColumn = memo<LogEntryMessageColumnProps>(
|
||||
({ isHighlighted, isHovered, isWrapped, segments }) => {
|
||||
const message = useMemo(() => segments.map(formatMessageSegment).join(''), [segments]);
|
||||
({ columnValue, highlights, isHighlighted, isHovered, isWrapped }) => {
|
||||
const message = useMemo(
|
||||
() =>
|
||||
isMessageColumn(columnValue)
|
||||
? formatMessageSegments(columnValue.message, highlights)
|
||||
: null,
|
||||
[columnValue, highlights]
|
||||
);
|
||||
|
||||
return (
|
||||
<MessageColumnContent
|
||||
|
@ -61,9 +73,25 @@ const MessageColumnContent = LogEntryColumnContent.extend.attrs<{
|
|||
${props => (props.isWrapped ? wrappedContentStyle : unwrappedContentStyle)};
|
||||
`;
|
||||
|
||||
const formatMessageSegment = (messageSegment: LogEntryMessageSegment): string => {
|
||||
const formatMessageSegments = (
|
||||
messageSegments: LogEntryMessageSegment[],
|
||||
highlights: LogEntryHighlightColumn[]
|
||||
) =>
|
||||
messageSegments.map((messageSegment, index) =>
|
||||
formatMessageSegment(
|
||||
messageSegment,
|
||||
highlights.map(highlight =>
|
||||
isHighlightMessageColumn(highlight) ? highlight.message[index].highlights : []
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const formatMessageSegment = (
|
||||
messageSegment: LogEntryMessageSegment,
|
||||
[firstHighlight = []]: string[][] // we only support one highlight for now
|
||||
): React.ReactNode => {
|
||||
if (isFieldSegment(messageSegment)) {
|
||||
return messageSegment.value;
|
||||
return highlightFieldValue(messageSegment.value, firstHighlight, HighlightMarker);
|
||||
} else if (isConstantSegment(messageSegment)) {
|
||||
return messageSegment.constant;
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import React, { useState, useCallback, useMemo } from 'react';
|
|||
import euiStyled from '../../../../../../common/eui_styled_components';
|
||||
import {
|
||||
LogEntry,
|
||||
isFieldColumn,
|
||||
isMessageColumn,
|
||||
LogEntryHighlight,
|
||||
LogEntryHighlightColumn,
|
||||
isTimestampColumn,
|
||||
} from '../../../utils/log_entry';
|
||||
import {
|
||||
|
@ -21,7 +21,7 @@ import {
|
|||
isFieldLogColumnConfiguration,
|
||||
} from '../../../utils/source_configuration';
|
||||
import { TextScale } from '../../../../common/log_text_scale';
|
||||
import { LogEntryColumn, LogEntryColumnWidth } from './log_entry_column';
|
||||
import { LogEntryColumn, LogEntryColumnWidths, iconColumnId } from './log_entry_column';
|
||||
import { LogEntryFieldColumn } from './log_entry_field_column';
|
||||
import { LogEntryDetailsIconColumn } from './log_entry_icon_column';
|
||||
import { LogEntryMessageColumn } from './log_entry_message_column';
|
||||
|
@ -31,7 +31,8 @@ import { monospaceTextStyle } from './text_styles';
|
|||
interface LogEntryRowProps {
|
||||
boundingBoxRef?: React.Ref<Element>;
|
||||
columnConfigurations: LogColumnConfiguration[];
|
||||
columnWidths: LogEntryColumnWidth[];
|
||||
columnWidths: LogEntryColumnWidths;
|
||||
highlights: LogEntryHighlight[];
|
||||
isHighlighted: boolean;
|
||||
logEntry: LogEntry;
|
||||
openFlyoutWithItem: (id: string) => void;
|
||||
|
@ -43,6 +44,7 @@ export const LogEntryRow = ({
|
|||
boundingBoxRef,
|
||||
columnConfigurations,
|
||||
columnWidths,
|
||||
highlights,
|
||||
isHighlighted,
|
||||
logEntry,
|
||||
openFlyoutWithItem,
|
||||
|
@ -64,7 +66,37 @@ export const LogEntryRow = ({
|
|||
logEntry.gid,
|
||||
]);
|
||||
|
||||
const iconColumnWidth = useMemo(() => columnWidths[columnWidths.length - 1], [columnWidths]);
|
||||
const logEntryColumnsById = useMemo(
|
||||
() =>
|
||||
logEntry.columns.reduce<{
|
||||
[columnId: string]: LogEntry['columns'][0];
|
||||
}>(
|
||||
(columnsById, column) => ({
|
||||
...columnsById,
|
||||
[column.columnId]: column,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
[logEntry.columns]
|
||||
);
|
||||
|
||||
const highlightsByColumnId = useMemo(
|
||||
() =>
|
||||
highlights.reduce<{
|
||||
[columnId: string]: LogEntryHighlightColumn[];
|
||||
}>(
|
||||
(columnsById, highlight) =>
|
||||
highlight.columns.reduce(
|
||||
(innerColumnsById, column) => ({
|
||||
...innerColumnsById,
|
||||
[column.columnId]: [...(innerColumnsById[column.columnId] || []), column],
|
||||
}),
|
||||
columnsById
|
||||
),
|
||||
{}
|
||||
),
|
||||
[highlights]
|
||||
);
|
||||
|
||||
return (
|
||||
<LogEntryRowWrapper
|
||||
|
@ -77,60 +109,76 @@ export const LogEntryRow = ({
|
|||
onMouseLeave={setItemIsNotHovered}
|
||||
scale={scale}
|
||||
>
|
||||
{logEntry.columns.map((column, columnIndex) => {
|
||||
const columnConfiguration = columnConfigurations[columnIndex];
|
||||
const columnWidth = columnWidths[columnIndex];
|
||||
{columnConfigurations.map(columnConfiguration => {
|
||||
if (isTimestampLogColumnConfiguration(columnConfiguration)) {
|
||||
const column = logEntryColumnsById[columnConfiguration.timestampColumn.id];
|
||||
const columnWidth = columnWidths[columnConfiguration.timestampColumn.id];
|
||||
|
||||
if (isTimestampColumn(column) && isTimestampLogColumnConfiguration(columnConfiguration)) {
|
||||
return (
|
||||
<LogEntryColumn
|
||||
data-test-subj="logColumn timestampLogColumn"
|
||||
key={columnConfiguration.timestampColumn.id}
|
||||
{...columnWidth}
|
||||
>
|
||||
<LogEntryTimestampColumn
|
||||
isHighlighted={isHighlighted}
|
||||
isHovered={isHovered}
|
||||
time={column.timestamp}
|
||||
/>
|
||||
{isTimestampColumn(column) ? (
|
||||
<LogEntryTimestampColumn
|
||||
isHighlighted={isHighlighted}
|
||||
isHovered={isHovered}
|
||||
time={column.timestamp}
|
||||
/>
|
||||
) : null}
|
||||
</LogEntryColumn>
|
||||
);
|
||||
} else if (
|
||||
isMessageColumn(column) &&
|
||||
isMessageLogColumnConfiguration(columnConfiguration)
|
||||
) {
|
||||
} else if (isMessageLogColumnConfiguration(columnConfiguration)) {
|
||||
const column = logEntryColumnsById[columnConfiguration.messageColumn.id];
|
||||
const columnWidth = columnWidths[columnConfiguration.messageColumn.id];
|
||||
|
||||
return (
|
||||
<LogEntryColumn
|
||||
data-test-subj="logColumn messageLogColumn"
|
||||
key={columnConfiguration.messageColumn.id}
|
||||
{...columnWidth}
|
||||
>
|
||||
<LogEntryMessageColumn
|
||||
isHighlighted={isHighlighted}
|
||||
isHovered={isHovered}
|
||||
isWrapped={wrap}
|
||||
segments={column.message}
|
||||
/>
|
||||
{column ? (
|
||||
<LogEntryMessageColumn
|
||||
columnValue={column}
|
||||
highlights={highlightsByColumnId[column.columnId] || []}
|
||||
isHighlighted={isHighlighted}
|
||||
isHovered={isHovered}
|
||||
isWrapped={wrap}
|
||||
/>
|
||||
) : null}
|
||||
</LogEntryColumn>
|
||||
);
|
||||
} else if (isFieldColumn(column) && isFieldLogColumnConfiguration(columnConfiguration)) {
|
||||
} else if (isFieldLogColumnConfiguration(columnConfiguration)) {
|
||||
const column = logEntryColumnsById[columnConfiguration.fieldColumn.id];
|
||||
const columnWidth = columnWidths[columnConfiguration.fieldColumn.id];
|
||||
|
||||
return (
|
||||
<LogEntryColumn
|
||||
data-test-subj={`logColumn fieldLogColumn fieldLogColumn:${column.field}`}
|
||||
data-test-subj={`logColumn fieldLogColumn fieldLogColumn:${
|
||||
columnConfiguration.fieldColumn.field
|
||||
}`}
|
||||
key={columnConfiguration.fieldColumn.id}
|
||||
{...columnWidth}
|
||||
>
|
||||
<LogEntryFieldColumn
|
||||
isHighlighted={isHighlighted}
|
||||
isHovered={isHovered}
|
||||
isWrapped={wrap}
|
||||
encodedValue={column.value}
|
||||
/>
|
||||
{column ? (
|
||||
<LogEntryFieldColumn
|
||||
columnValue={column}
|
||||
highlights={highlightsByColumnId[column.columnId] || []}
|
||||
isHighlighted={isHighlighted}
|
||||
isHovered={isHovered}
|
||||
isWrapped={wrap}
|
||||
/>
|
||||
) : null}
|
||||
</LogEntryColumn>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<LogEntryColumn key="logColumn iconLogColumn iconLogColumn:details" {...iconColumnWidth}>
|
||||
<LogEntryColumn
|
||||
key="logColumn iconLogColumn iconLogColumn:details"
|
||||
{...columnWidths[iconColumnId]}
|
||||
>
|
||||
<LogEntryDetailsIconColumn
|
||||
isHighlighted={isHighlighted}
|
||||
isHovered={isHovered}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { LogTextStreamLoadingItemView } from './loading_item_view';
|
|||
import { LogEntryRow } from './log_entry_row';
|
||||
import { MeasurableItemView } from './measurable_item_view';
|
||||
import { VerticalScrollPanel } from './vertical_scroll_panel';
|
||||
import { getColumnWidths, LogEntryColumnWidth } from './log_entry_column';
|
||||
import { getColumnWidths, LogEntryColumnWidths } from './log_entry_column';
|
||||
import { useMeasuredCharacterDimensions } from './text_styles';
|
||||
|
||||
interface ScrollableLogTextStreamViewProps {
|
||||
|
@ -188,6 +188,7 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
|
|||
openFlyoutWithItem={this.handleOpenFlyout}
|
||||
boundingBoxRef={itemMeasureRef}
|
||||
logEntry={item.logEntry}
|
||||
highlights={item.highlights}
|
||||
scale={scale}
|
||||
wrap={wrap}
|
||||
isHighlighted={
|
||||
|
@ -281,7 +282,7 @@ export const ScrollableLogTextStreamView = injectI18n(ScrollableLogTextStreamVie
|
|||
*/
|
||||
const WithColumnWidths: React.FunctionComponent<{
|
||||
children: (
|
||||
params: { columnWidths: LogEntryColumnWidth[]; CharacterDimensionsProbe: React.ComponentType }
|
||||
params: { columnWidths: LogEntryColumnWidths; CharacterDimensionsProbe: React.ComponentType }
|
||||
) => React.ReactElement<any> | null;
|
||||
columnConfigurations: LogColumnConfiguration[];
|
||||
scale: TextScale;
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
import { EuiBadge, EuiButton, EuiPopover, EuiPopoverTitle, EuiSelectable } from '@elastic/eui';
|
||||
import { Option } from '@elastic/eui/src/components/selectable/types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { LogColumnConfiguration } from '../../utils/source_configuration';
|
||||
import { useVisibilityState } from '../../utils/use_visibility_state';
|
||||
import { euiStyled } from '../../../../../common/eui_styled_components';
|
||||
|
||||
interface SelectableColumnOption {
|
||||
|
@ -23,7 +24,7 @@ export const AddLogColumnButtonAndPopover: React.FunctionComponent<{
|
|||
availableFields: string[];
|
||||
isDisabled?: boolean;
|
||||
}> = ({ addLogColumn, availableFields, isDisabled }) => {
|
||||
const [isOpen, openPopover, closePopover] = usePopoverVisibilityState(false);
|
||||
const { isVisible: isOpen, show: openPopover, hide: closePopover } = useVisibilityState(false);
|
||||
|
||||
const availableColumnOptions = useMemo<SelectableColumnOption[]>(
|
||||
() => [
|
||||
|
@ -146,18 +147,6 @@ const selectableListProps = {
|
|||
showIcons: false,
|
||||
};
|
||||
|
||||
const usePopoverVisibilityState = (initialState: boolean) => {
|
||||
const [isOpen, setIsOpen] = useState(initialState);
|
||||
|
||||
const closePopover = useCallback(() => setIsOpen(false), []);
|
||||
const openPopover = useCallback(() => setIsOpen(true), []);
|
||||
|
||||
return useMemo<[typeof isOpen, typeof openPopover, typeof closePopover]>(
|
||||
() => [isOpen, openPopover, closePopover],
|
||||
[isOpen, openPopover, closePopover]
|
||||
);
|
||||
};
|
||||
|
||||
const SystemColumnBadge: React.FunctionComponent = () => (
|
||||
<EuiBadge>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
import createContainer from 'constate-latest';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useVisibilityState } from '../../utils/use_visibility_state';
|
||||
|
||||
type TabId = 'indicesAndFieldsTab' | 'logsTab';
|
||||
const validTabIds: TabId[] = ['indicesAndFieldsTab', 'logsTab'];
|
||||
|
||||
|
@ -17,33 +19,27 @@ export const useSourceConfigurationFlyoutState = ({
|
|||
initialVisibility?: boolean;
|
||||
initialTab?: TabId;
|
||||
} = {}) => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(initialVisibility);
|
||||
const { isVisible, show, hide, toggle: toggleIsVisible } = useVisibilityState(initialVisibility);
|
||||
const [activeTabId, setActiveTab] = useState(initialTab);
|
||||
|
||||
const toggleIsVisible = useCallback(
|
||||
() => setIsVisible(isCurrentlyVisible => !isCurrentlyVisible),
|
||||
[setIsVisible]
|
||||
);
|
||||
|
||||
const show = useCallback(
|
||||
const showWithTab = useCallback(
|
||||
(tabId?: TabId) => {
|
||||
if (tabId != null) {
|
||||
setActiveTab(tabId);
|
||||
}
|
||||
setIsVisible(true);
|
||||
show();
|
||||
},
|
||||
[setIsVisible]
|
||||
[show]
|
||||
);
|
||||
const showIndicesConfiguration = useCallback(() => show('indicesAndFieldsTab'), [show]);
|
||||
const showLogsConfiguration = useCallback(() => show('logsTab'), [show]);
|
||||
const hide = useCallback(() => setIsVisible(false), [setIsVisible]);
|
||||
const showIndicesConfiguration = useCallback(() => showWithTab('indicesAndFieldsTab'), [show]);
|
||||
const showLogsConfiguration = useCallback(() => showWithTab('logsTab'), [show]);
|
||||
|
||||
return {
|
||||
activeTabId,
|
||||
hide,
|
||||
isVisible,
|
||||
setActiveTab,
|
||||
show,
|
||||
show: showWithTab,
|
||||
showIndicesConfiguration,
|
||||
showLogsConfiguration,
|
||||
toggleIsVisible,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 './log_highlights';
|
||||
export * from './redux_bridges';
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
import { sharedFragments } from '../../../../common/graphql/shared';
|
||||
|
||||
export const logEntryHighlightsQuery = gql`
|
||||
query LogEntryHighlightsQuery(
|
||||
$sourceId: ID = "default"
|
||||
$startKey: InfraTimeKeyInput!
|
||||
$endKey: InfraTimeKeyInput!
|
||||
$filterQuery: String
|
||||
$highlights: [InfraLogEntryHighlightInput!]!
|
||||
) {
|
||||
source(id: $sourceId) {
|
||||
id
|
||||
logEntryHighlights(
|
||||
startKey: $startKey
|
||||
endKey: $endKey
|
||||
filterQuery: $filterQuery
|
||||
highlights: $highlights
|
||||
) {
|
||||
start {
|
||||
...InfraTimeKeyFields
|
||||
}
|
||||
end {
|
||||
...InfraTimeKeyFields
|
||||
}
|
||||
entries {
|
||||
...InfraLogEntryHighlightFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${sharedFragments.InfraTimeKey}
|
||||
${sharedFragments.InfraLogEntryHighlightFields}
|
||||
`;
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
import { TimeKey, getPreviousTimeKey, getNextTimeKey } from '../../../../common/time';
|
||||
import { LogEntryHighlightsQuery } from '../../../graphql/types';
|
||||
import { DependencyError, useApolloClient } from '../../../utils/apollo_context';
|
||||
import { LogEntryHighlightsMap } from '../../../utils/log_entry';
|
||||
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
|
||||
import { logEntryHighlightsQuery } from './log_highlights.gql_query';
|
||||
|
||||
type LogEntryHighlights = LogEntryHighlightsQuery.Query['source']['logEntryHighlights'];
|
||||
|
||||
export const useLogHighlightsState = ({
|
||||
sourceId,
|
||||
sourceVersion,
|
||||
}: {
|
||||
sourceId: string;
|
||||
sourceVersion: string | undefined;
|
||||
}) => {
|
||||
const [highlightTerms, setHighlightTerms] = useState<string[]>([]);
|
||||
const apolloClient = useApolloClient();
|
||||
const [logEntryHighlights, setLogEntryHighlights] = useState<LogEntryHighlights | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [startKey, setStartKey] = useState<TimeKey | null>(null);
|
||||
const [endKey, setEndKey] = useState<TimeKey | null>(null);
|
||||
const [filterQuery, setFilterQuery] = useState<string | null>(null);
|
||||
|
||||
const [loadLogEntryHighlightsRequest, loadLogEntryHighlights] = useTrackedPromise(
|
||||
{
|
||||
cancelPreviousOn: 'resolution',
|
||||
createPromise: async () => {
|
||||
if (!apolloClient) {
|
||||
throw new DependencyError('Failed to load source: No apollo client available.');
|
||||
}
|
||||
if (!startKey || !endKey || !highlightTerms.length) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return await apolloClient.query<
|
||||
LogEntryHighlightsQuery.Query,
|
||||
LogEntryHighlightsQuery.Variables
|
||||
>({
|
||||
fetchPolicy: 'no-cache',
|
||||
query: logEntryHighlightsQuery,
|
||||
variables: {
|
||||
sourceId,
|
||||
startKey: getPreviousTimeKey(startKey), // interval boundaries are exclusive
|
||||
endKey: getNextTimeKey(endKey), // interval boundaries are exclusive
|
||||
filterQuery,
|
||||
highlights: [
|
||||
{
|
||||
query: JSON.stringify({
|
||||
multi_match: { query: highlightTerms[0], type: 'phrase', lenient: true },
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
onResolve: response => {
|
||||
setLogEntryHighlights(response.data.source.logEntryHighlights);
|
||||
},
|
||||
},
|
||||
[apolloClient, sourceId, startKey, endKey, filterQuery, highlightTerms]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
highlightTerms.filter(highlightTerm => highlightTerm.length > 0).length &&
|
||||
startKey &&
|
||||
endKey
|
||||
) {
|
||||
loadLogEntryHighlights();
|
||||
} else {
|
||||
setLogEntryHighlights(undefined);
|
||||
}
|
||||
},
|
||||
[highlightTerms, startKey, endKey, filterQuery, sourceVersion]
|
||||
);
|
||||
|
||||
const logEntryHighlightsById = useMemo(
|
||||
() =>
|
||||
logEntryHighlights
|
||||
? logEntryHighlights.reduce<LogEntryHighlightsMap>(
|
||||
(accumulatedLogEntryHighlightsById, { entries }) => {
|
||||
return entries.reduce<LogEntryHighlightsMap>(
|
||||
(singleHighlightLogEntriesById, entry) => {
|
||||
const highlightsForId = singleHighlightLogEntriesById[entry.gid] || [];
|
||||
return {
|
||||
...singleHighlightLogEntriesById,
|
||||
[entry.gid]: [...highlightsForId, entry],
|
||||
};
|
||||
},
|
||||
accumulatedLogEntryHighlightsById
|
||||
);
|
||||
},
|
||||
{}
|
||||
)
|
||||
: {},
|
||||
[logEntryHighlights]
|
||||
);
|
||||
|
||||
return {
|
||||
highlightTerms,
|
||||
setHighlightTerms,
|
||||
setStartKey,
|
||||
setEndKey,
|
||||
setFilterQuery,
|
||||
logEntryHighlights,
|
||||
logEntryHighlightsById,
|
||||
loadLogEntryHighlightsRequest,
|
||||
};
|
||||
};
|
||||
|
||||
export const LogHighlightsState = createContainer(useLogHighlightsState);
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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, { useEffect, useContext } from 'react';
|
||||
|
||||
import { TimeKey } from '../../../../common/time';
|
||||
import { withLogFilter } from '../with_log_filter';
|
||||
import { withStreamItems } from '../with_stream_items';
|
||||
import { LogHighlightsState } from './log_highlights';
|
||||
|
||||
// Bridges Redux container state with Hooks state. Once state is moved fully from
|
||||
// Redux to Hooks this can be removed.
|
||||
export const LogHighlightsPositionBridge = withStreamItems(
|
||||
({ entriesStart, entriesEnd }: { entriesStart: TimeKey | null; entriesEnd: TimeKey | null }) => {
|
||||
const { setStartKey, setEndKey } = useContext(LogHighlightsState.Context);
|
||||
useEffect(
|
||||
() => {
|
||||
setStartKey(entriesStart);
|
||||
setEndKey(entriesEnd);
|
||||
},
|
||||
[entriesStart, entriesEnd]
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
export const LogHighlightsFilterQueryBridge = withLogFilter(
|
||||
({ serializedFilterQuery }: { serializedFilterQuery: string | null }) => {
|
||||
const { setFilterQuery } = useContext(LogHighlightsState.Context);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setFilterQuery(serializedFilterQuery);
|
||||
},
|
||||
[serializedFilterQuery]
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
export const LogHighlightsBridge = ({ indexPattern }: { indexPattern: any }) => (
|
||||
<>
|
||||
<LogHighlightsPositionBridge />
|
||||
<LogHighlightsFilterQueryBridge indexPattern={indexPattern} />
|
||||
</>
|
||||
);
|
|
@ -19,9 +19,10 @@ interface WithLogFilterProps {
|
|||
indexPattern: StaticIndexPattern;
|
||||
}
|
||||
|
||||
const withLogFilter = connect(
|
||||
export const withLogFilter = connect(
|
||||
(state: State) => ({
|
||||
filterQuery: logFilterSelectors.selectLogFilterQuery(state),
|
||||
serializedFilterQuery: logFilterSelectors.selectLogFilterQueryAsJson(state),
|
||||
filterQueryDraft: logFilterSelectors.selectLogFilterQueryDraft(state),
|
||||
isFilterQueryDraftValid: logFilterSelectors.selectIsLogFilterQueryDraftValid(state),
|
||||
}),
|
||||
|
|
|
@ -4,23 +4,29 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useContext, useMemo } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { StreamItem, LogEntryStreamItem } from '../../components/logging/log_text_stream/item';
|
||||
import { logEntriesActions, logEntriesSelectors, logPositionSelectors, State } from '../../store';
|
||||
import { LogEntry } from '../../utils/log_entry';
|
||||
import { asChildFunctionRenderer } from '../../utils/typed_react';
|
||||
import { LogEntry, LogEntryHighlight } from '../../utils/log_entry';
|
||||
import { PropsOfContainer, RendererFunction } from '../../utils/typed_react';
|
||||
import { bindPlainActionCreators } from '../../utils/typed_redux';
|
||||
// deep inporting to avoid a circular import problem
|
||||
import { LogHighlightsState } from './log_highlights/log_highlights';
|
||||
|
||||
export const withStreamItems = connect(
|
||||
(state: State) => ({
|
||||
isAutoReloading: logPositionSelectors.selectIsAutoReloading(state),
|
||||
isReloading: logEntriesSelectors.selectIsReloadingEntries(state),
|
||||
isLoadingMore: logEntriesSelectors.selectIsLoadingMoreEntries(state),
|
||||
hasMoreBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart(state),
|
||||
hasMoreAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd(state),
|
||||
lastLoadedTime: logEntriesSelectors.selectEntriesLastLoadedTime(state),
|
||||
items: selectItems(state),
|
||||
entries: logEntriesSelectors.selectEntries(state),
|
||||
entriesStart: logEntriesSelectors.selectEntriesStart(state),
|
||||
entriesEnd: logEntriesSelectors.selectEntriesEnd(state),
|
||||
// items: selectItems(state),
|
||||
}),
|
||||
bindPlainActionCreators({
|
||||
loadNewerEntries: logEntriesActions.loadNewerEntries,
|
||||
|
@ -29,30 +35,73 @@ export const withStreamItems = connect(
|
|||
})
|
||||
);
|
||||
|
||||
export const WithStreamItems = asChildFunctionRenderer(withStreamItems, {
|
||||
onInitialize: props => {
|
||||
if (!props.isReloading && !props.isLoadingMore) {
|
||||
props.reloadEntries();
|
||||
}
|
||||
},
|
||||
});
|
||||
type WithStreamItemsProps = PropsOfContainer<typeof withStreamItems>;
|
||||
|
||||
const selectItems = createSelector(
|
||||
logEntriesSelectors.selectEntries,
|
||||
logEntriesSelectors.selectIsReloadingEntries,
|
||||
logPositionSelectors.selectIsAutoReloading,
|
||||
// searchResultsSelectors.selectSearchResultsById,
|
||||
(logEntries, isReloading, isAutoReloading /* , searchResults */) =>
|
||||
isReloading && !isAutoReloading
|
||||
? []
|
||||
: logEntries.map(logEntry =>
|
||||
createLogEntryStreamItem(logEntry /* , searchResults[logEntry.gid] || null */)
|
||||
)
|
||||
export const WithStreamItems = withStreamItems(
|
||||
({
|
||||
children,
|
||||
initializeOnMount,
|
||||
...props
|
||||
}: WithStreamItemsProps & {
|
||||
children: RendererFunction<
|
||||
WithStreamItemsProps & {
|
||||
items: StreamItem[];
|
||||
}
|
||||
>;
|
||||
initializeOnMount: boolean;
|
||||
}) => {
|
||||
const { logEntryHighlightsById } = useContext(LogHighlightsState.Context);
|
||||
const items = useMemo(
|
||||
() =>
|
||||
props.isReloading && !props.isAutoReloading
|
||||
? []
|
||||
: props.entries.map(logEntry =>
|
||||
createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || [])
|
||||
),
|
||||
[props.isReloading, props.isAutoReloading, props.entries, logEntryHighlightsById]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (initializeOnMount && !props.isReloading && !props.isLoadingMore) {
|
||||
props.reloadEntries();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return children({
|
||||
...props,
|
||||
items,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const createLogEntryStreamItem = (logEntry: LogEntry) => ({
|
||||
// export const WithStreamItemsOld = asChildFunctionRenderer(withStreamItems, {
|
||||
// onInitialize: props => {
|
||||
// if (!props.isReloading && !props.isLoadingMore) {
|
||||
// props.reloadEntries();
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
|
||||
// const selectItems = createSelector(
|
||||
// logEntriesSelectors.selectEntries,
|
||||
// logEntriesSelectors.selectIsReloadingEntries,
|
||||
// logPositionSelectors.selectIsAutoReloading,
|
||||
// // searchResultsSelectors.selectSearchResultsById,
|
||||
// (logEntries, isReloading, isAutoReloading /* , searchResults */) =>
|
||||
// isReloading && !isAutoReloading
|
||||
// ? []
|
||||
// : logEntries.map(logEntry =>
|
||||
// createLogEntryStreamItem(logEntry /* , searchResults[logEntry.gid] || null */)
|
||||
// )
|
||||
// );
|
||||
|
||||
const createLogEntryStreamItem = (
|
||||
logEntry: LogEntry,
|
||||
highlights: LogEntryHighlight[]
|
||||
): LogEntryStreamItem => ({
|
||||
kind: 'logEntry' as 'logEntry',
|
||||
logEntry,
|
||||
highlights,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { Source } from './source';
|
||||
export { Source, useSource } from './source';
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
UpdateSourceInput,
|
||||
UpdateSourceMutation,
|
||||
} from '../../graphql/types';
|
||||
import { useApolloClient } from '../../utils/apollo_context';
|
||||
import { DependencyError, useApolloClient } from '../../utils/apollo_context';
|
||||
import { useTrackedPromise } from '../../utils/use_tracked_promise';
|
||||
import { createSourceMutation } from './create_source.gql_query';
|
||||
import { sourceQuery } from './query_source.gql_query';
|
||||
|
@ -167,10 +167,3 @@ export const useSource = ({ sourceId }: { sourceId: string }) => {
|
|||
};
|
||||
|
||||
export const Source = createContainer(useSource);
|
||||
|
||||
class DependencyError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -201,12 +201,6 @@
|
|||
"description": "The query to filter the log entries by",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "highlightQuery",
|
||||
"description": "The query to highlight the log entries with",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
|
@ -246,12 +240,6 @@
|
|||
"description": "The query to filter the log entries by",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "highlightQuery",
|
||||
"description": "The query to highlight the log entries with",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
|
@ -262,6 +250,75 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "logEntryHighlights",
|
||||
"description": "Sequences of log entries matching sets of highlighting queries within an interval",
|
||||
"args": [
|
||||
{
|
||||
"name": "startKey",
|
||||
"description": "The sort key that corresponds to the start of the interval",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "INPUT_OBJECT", "name": "InfraTimeKeyInput", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "endKey",
|
||||
"description": "The sort key that corresponds to the end of the interval",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "INPUT_OBJECT", "name": "InfraTimeKeyInput", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "filterQuery",
|
||||
"description": "The query to filter the log entries by",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "highlights",
|
||||
"description": "The highlighting to apply to the log entries",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "InfraLogEntryHighlightInput",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "OBJECT", "name": "InfraLogEntryInterval", "ofType": null }
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "logSummaryBetween",
|
||||
"description": "A consecutive span of summary buckets within an interval",
|
||||
|
@ -1387,6 +1444,18 @@
|
|||
"name": "InfraLogEntryTimestampColumn",
|
||||
"description": "A special built-in column that contains the log entry's timestamp",
|
||||
"fields": [
|
||||
{
|
||||
"name": "columnId",
|
||||
"description": "The id of the corresponding column configuration",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "timestamp",
|
||||
"description": "The timestamp",
|
||||
|
@ -1410,6 +1479,18 @@
|
|||
"name": "InfraLogEntryMessageColumn",
|
||||
"description": "A special built-in column that contains the log entry's constructed message",
|
||||
"fields": [
|
||||
{
|
||||
"name": "columnId",
|
||||
"description": "The id of the corresponding column configuration",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"description": "A list of the formatted log entry segments",
|
||||
|
@ -1532,6 +1613,18 @@
|
|||
"name": "InfraLogEntryFieldColumn",
|
||||
"description": "A column that contains the value of a field of the log entry",
|
||||
"fields": [
|
||||
{
|
||||
"name": "columnId",
|
||||
"description": "The id of the corresponding column configuration",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"description": "The field name of the column",
|
||||
|
@ -1555,6 +1648,26 @@
|
|||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "highlights",
|
||||
"description": "A list of highlighted substrings of the value",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
@ -1562,6 +1675,27 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "InfraLogEntryHighlightInput",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "query",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "InfraLogSummaryInterval",
|
||||
|
|
|
@ -34,6 +34,8 @@ export interface InfraSource {
|
|||
logEntriesAround: InfraLogEntryInterval;
|
||||
/** A consecutive span of log entries within an interval */
|
||||
logEntriesBetween: InfraLogEntryInterval;
|
||||
/** Sequences of log entries matching sets of highlighting queries within an interval */
|
||||
logEntryHighlights: InfraLogEntryInterval[];
|
||||
/** A consecutive span of summary buckets within an interval */
|
||||
logSummaryBetween: InfraLogSummaryInterval;
|
||||
|
||||
|
@ -181,11 +183,15 @@ export interface InfraLogEntry {
|
|||
}
|
||||
/** A special built-in column that contains the log entry's timestamp */
|
||||
export interface InfraLogEntryTimestampColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** The timestamp */
|
||||
timestamp: number;
|
||||
}
|
||||
/** A special built-in column that contains the log entry's constructed message */
|
||||
export interface InfraLogEntryMessageColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** A list of the formatted log entry segments */
|
||||
message: InfraLogMessageSegment[];
|
||||
}
|
||||
|
@ -205,10 +211,14 @@ export interface InfraLogMessageConstantSegment {
|
|||
}
|
||||
/** A column that contains the value of a field of the log entry */
|
||||
export interface InfraLogEntryFieldColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** The field name of the column */
|
||||
field: string;
|
||||
/** The value of the field in the log entry */
|
||||
value: string;
|
||||
/** A list of highlighted substrings of the value */
|
||||
highlights: string[];
|
||||
}
|
||||
/** A consecutive sequence of log summary buckets */
|
||||
export interface InfraLogSummaryInterval {
|
||||
|
@ -325,6 +335,10 @@ export interface InfraTimeKeyInput {
|
|||
tiebreaker: number;
|
||||
}
|
||||
|
||||
export interface InfraLogEntryHighlightInput {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface InfraTimerangeInput {
|
||||
/** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
|
||||
interval: string;
|
||||
|
@ -419,8 +433,6 @@ export interface LogEntriesAroundInfraSourceArgs {
|
|||
countAfter?: number | null;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The query to highlight the log entries with */
|
||||
highlightQuery?: string | null;
|
||||
}
|
||||
export interface LogEntriesBetweenInfraSourceArgs {
|
||||
/** The sort key that corresponds to the start of the interval */
|
||||
|
@ -429,8 +441,16 @@ export interface LogEntriesBetweenInfraSourceArgs {
|
|||
endKey: InfraTimeKeyInput;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The query to highlight the log entries with */
|
||||
highlightQuery?: string | null;
|
||||
}
|
||||
export interface LogEntryHighlightsInfraSourceArgs {
|
||||
/** The sort key that corresponds to the start of the interval */
|
||||
startKey: InfraTimeKeyInput;
|
||||
/** The sort key that corresponds to the end of the interval */
|
||||
endKey: InfraTimeKeyInput;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The highlighting to apply to the log entries */
|
||||
highlights: InfraLogEntryHighlightInput[];
|
||||
}
|
||||
export interface LogSummaryBetweenInfraSourceArgs {
|
||||
/** The millisecond timestamp that corresponds to the start of the interval */
|
||||
|
@ -612,6 +632,46 @@ export namespace FlyoutItemQuery {
|
|||
};
|
||||
}
|
||||
|
||||
export namespace LogEntryHighlightsQuery {
|
||||
export type Variables = {
|
||||
sourceId?: string | null;
|
||||
startKey: InfraTimeKeyInput;
|
||||
endKey: InfraTimeKeyInput;
|
||||
filterQuery?: string | null;
|
||||
highlights: InfraLogEntryHighlightInput[];
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'InfraSource';
|
||||
|
||||
id: string;
|
||||
|
||||
logEntryHighlights: LogEntryHighlights[];
|
||||
};
|
||||
|
||||
export type LogEntryHighlights = {
|
||||
__typename?: 'InfraLogEntryInterval';
|
||||
|
||||
start?: Start | null;
|
||||
|
||||
end?: End | null;
|
||||
|
||||
entries: Entries[];
|
||||
};
|
||||
|
||||
export type Start = InfraTimeKeyFields.Fragment;
|
||||
|
||||
export type End = InfraTimeKeyFields.Fragment;
|
||||
|
||||
export type Entries = InfraLogEntryHighlightFields.Fragment;
|
||||
}
|
||||
|
||||
export namespace LogSummary {
|
||||
export type Variables = {
|
||||
sourceId?: string | null;
|
||||
|
@ -1085,12 +1145,16 @@ export namespace InfraLogEntryFields {
|
|||
export type InfraLogEntryTimestampColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryTimestampColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type InfraLogEntryMessageColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryMessageColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
message: Message[];
|
||||
};
|
||||
|
||||
|
@ -1115,8 +1179,62 @@ export namespace InfraLogEntryFields {
|
|||
export type InfraLogEntryFieldColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryFieldColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
field: string;
|
||||
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace InfraLogEntryHighlightFields {
|
||||
export type Fragment = {
|
||||
__typename?: 'InfraLogEntry';
|
||||
|
||||
gid: string;
|
||||
|
||||
key: Key;
|
||||
|
||||
columns: Columns[];
|
||||
};
|
||||
|
||||
export type Key = {
|
||||
__typename?: 'InfraTimeKey';
|
||||
|
||||
time: number;
|
||||
|
||||
tiebreaker: number;
|
||||
};
|
||||
|
||||
export type Columns =
|
||||
| InfraLogEntryMessageColumnInlineFragment
|
||||
| InfraLogEntryFieldColumnInlineFragment;
|
||||
|
||||
export type InfraLogEntryMessageColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryMessageColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
message: Message[];
|
||||
};
|
||||
|
||||
export type Message = InfraLogMessageFieldSegmentInlineFragment;
|
||||
|
||||
export type InfraLogMessageFieldSegmentInlineFragment = {
|
||||
__typename?: 'InfraLogMessageFieldSegment';
|
||||
|
||||
field: string;
|
||||
|
||||
highlights: string[];
|
||||
};
|
||||
|
||||
export type InfraLogEntryFieldColumnInlineFragment = {
|
||||
__typename?: 'InfraLogEntryFieldColumn';
|
||||
|
||||
columnId: string;
|
||||
|
||||
field: string;
|
||||
|
||||
highlights: string[];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { Source } from '../../containers/source';
|
|||
|
||||
import { LogsToolbar } from './page_toolbar';
|
||||
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
|
||||
import { LogHighlightsBridge } from '../../containers/logs/log_highlights';
|
||||
|
||||
export const LogsPageLogsContent: React.FunctionComponent = () => {
|
||||
const { derivedIndexPattern, source, sourceId, version } = useContext(Source.Context);
|
||||
|
@ -47,6 +48,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
|
|||
return (
|
||||
<>
|
||||
<ReduxSourceIdBridge sourceId={sourceId} />
|
||||
<LogHighlightsBridge indexPattern={derivedIndexPattern} />
|
||||
<WithLogFilterUrlState indexPattern={derivedIndexPattern} />
|
||||
<WithLogPositionUrlState />
|
||||
<WithLogMinimapUrlState />
|
||||
|
|
|
@ -9,19 +9,25 @@ import React from 'react';
|
|||
import { SourceConfigurationFlyoutState } from '../../components/source_configuration';
|
||||
import { LogFlyout } from '../../containers/logs/log_flyout';
|
||||
import { LogViewConfiguration } from '../../containers/logs/log_view_configuration';
|
||||
import { Source } from '../../containers/source';
|
||||
import { LogHighlightsState } from '../../containers/logs/log_highlights/log_highlights';
|
||||
import { Source, useSource } from '../../containers/source';
|
||||
import { useSourceId } from '../../containers/source_id';
|
||||
|
||||
export const LogsPageProviders: React.FunctionComponent = ({ children }) => {
|
||||
const [sourceId] = useSourceId();
|
||||
const source = useSource({ sourceId });
|
||||
|
||||
return (
|
||||
<Source.Provider sourceId={sourceId}>
|
||||
<Source.Context.Provider value={source}>
|
||||
<SourceConfigurationFlyoutState.Provider>
|
||||
<LogViewConfiguration.Provider>
|
||||
<LogFlyout.Provider>{children}</LogFlyout.Provider>
|
||||
<LogFlyout.Provider>
|
||||
<LogHighlightsState.Provider sourceId={sourceId} sourceVersion={source.version}>
|
||||
{children}
|
||||
</LogHighlightsState.Provider>
|
||||
</LogFlyout.Provider>
|
||||
</LogViewConfiguration.Provider>
|
||||
</SourceConfigurationFlyoutState.Provider>
|
||||
</Source.Provider>
|
||||
</Source.Context.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,6 +11,8 @@ import React, { useContext } from 'react';
|
|||
import { AutocompleteField } from '../../components/autocomplete_field';
|
||||
import { Toolbar } from '../../components/eui';
|
||||
import { LogCustomizationMenu } from '../../components/logging/log_customization_menu';
|
||||
import { LogHighlightsMenu } from '../../components/logging/log_highlights_menu';
|
||||
import { LogHighlightsState } from '../../containers/logs/log_highlights/log_highlights';
|
||||
import { LogMinimapScaleControls } from '../../components/logging/log_minimap_scale_controls';
|
||||
import { LogTextScaleControls } from '../../components/logging/log_text_scale_controls';
|
||||
import { LogTextWrapControls } from '../../components/logging/log_text_wrap_controls';
|
||||
|
@ -37,6 +39,10 @@ export const LogsToolbar = injectI18n(({ intl }) => {
|
|||
} = useContext(LogViewConfiguration.Context);
|
||||
|
||||
const { setSurroundingLogsId } = useContext(LogFlyout.Context);
|
||||
|
||||
const { setHighlightTerms, loadLogEntryHighlightsRequest, highlightTerms } = useContext(
|
||||
LogHighlightsState.Context
|
||||
);
|
||||
return (
|
||||
<Toolbar>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="s">
|
||||
|
@ -92,6 +98,15 @@ export const LogsToolbar = injectI18n(({ intl }) => {
|
|||
/>
|
||||
</LogCustomizationMenu>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogHighlightsMenu
|
||||
onChange={setHighlightTerms}
|
||||
isLoading={loadLogEntryHighlightsRequest.state === 'pending'}
|
||||
activeHighlights={
|
||||
highlightTerms.filter(highlightTerm => highlightTerm.length > 0).length > 0
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<WithLogPosition resetOnUnmount>
|
||||
{({
|
||||
|
|
|
@ -17,3 +17,10 @@ export const ApolloClientContext = createContext<ApolloClient<{}> | undefined>(u
|
|||
export const useApolloClient = () => {
|
||||
return useContext(ApolloClientContext);
|
||||
};
|
||||
|
||||
export class DependencyError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export * from './log_entry';
|
||||
export * from './log_entry_highlight';
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { InfraLogEntryHighlightFields } from '../../graphql/types';
|
||||
|
||||
export type LogEntryHighlight = InfraLogEntryHighlightFields.Fragment;
|
||||
|
||||
export type LogEntryHighlightColumn = InfraLogEntryHighlightFields.Columns;
|
||||
export type LogEntryHighlightMessageColumn = InfraLogEntryHighlightFields.InfraLogEntryMessageColumnInlineFragment;
|
||||
export type LogEntryHighlightFieldColumn = InfraLogEntryHighlightFields.InfraLogEntryFieldColumnInlineFragment;
|
||||
|
||||
export type LogEntryHighlightMessageSegment = InfraLogEntryHighlightFields.Message | {};
|
||||
export type LogEntryHighlightFieldMessageSegment = InfraLogEntryHighlightFields.InfraLogMessageFieldSegmentInlineFragment;
|
||||
|
||||
export interface LogEntryHighlightsMap {
|
||||
[entryId: string]: LogEntryHighlight[];
|
||||
}
|
||||
|
||||
export const isHighlightMessageColumn = (
|
||||
column: LogEntryHighlightColumn
|
||||
): column is LogEntryHighlightMessageColumn => column != null && 'message' in column;
|
||||
|
||||
export const isHighlightFieldColumn = (
|
||||
column: LogEntryHighlightColumn
|
||||
): column is LogEntryHighlightFieldColumn => column != null && 'field' in column;
|
||||
|
||||
export const isHighlightFieldSegment = (
|
||||
segment: LogEntryHighlightMessageSegment
|
||||
): segment is LogEntryHighlightFieldMessageSegment =>
|
||||
segment && 'field' in segment && 'highlights' in segment;
|
|
@ -39,6 +39,13 @@ export const ifProp = <Pass, Fail>(
|
|||
fail: Fail
|
||||
) => (props: object) => (asPropReader(propName)(props) ? pass : fail);
|
||||
|
||||
export const tintOrShade = (textColor: 'string', color: 'string', fraction: number) => {
|
||||
return parseToHsl(textColor).lightness > 0.5 ? shade(fraction, color) : tint(fraction, color);
|
||||
export const tintOrShade = (
|
||||
textColor: string,
|
||||
color: string,
|
||||
tintFraction: number,
|
||||
shadeFraction: number
|
||||
) => {
|
||||
return parseToHsl(textColor).lightness > 0.5
|
||||
? shade(1 - shadeFraction, color)
|
||||
: tint(1 - tintFraction, color);
|
||||
};
|
||||
|
|
|
@ -59,6 +59,13 @@ export type StateUpdater<State, Props = {}> = (
|
|||
prevProps: Readonly<Props>
|
||||
) => State | null;
|
||||
|
||||
export type PropsOfContainer<Container> = Container extends InferableComponentEnhancerWithProps<
|
||||
infer InjectedProps,
|
||||
any
|
||||
>
|
||||
? InjectedProps
|
||||
: never;
|
||||
|
||||
export function composeStateUpdaters<State, Props>(...updaters: Array<StateUpdater<State, Props>>) {
|
||||
return (state: State, props: Props) =>
|
||||
updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state);
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
export const useVisibilityState = (initialState: boolean) => {
|
||||
const [isVisible, setIsVisible] = useState(initialState);
|
||||
|
||||
const hide = useCallback(() => setIsVisible(false), []);
|
||||
const show = useCallback(() => setIsVisible(true), []);
|
||||
const toggle = useCallback(() => setIsVisible(state => !state), []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
hide,
|
||||
isVisible,
|
||||
show,
|
||||
toggle,
|
||||
}),
|
||||
[isVisible, show, hide]
|
||||
);
|
||||
};
|
|
@ -6,9 +6,11 @@
|
|||
|
||||
import { failure } from 'io-ts/lib/PathReporter';
|
||||
|
||||
import { JsonObject } from '../../../common/typed_json';
|
||||
import {
|
||||
InfraLogEntryColumn,
|
||||
InfraLogEntryFieldColumn,
|
||||
InfraLogEntryHighlightInput,
|
||||
InfraLogEntryMessageColumn,
|
||||
InfraLogEntryTimestampColumn,
|
||||
InfraLogMessageConstantSegment,
|
||||
|
@ -33,6 +35,11 @@ export type InfraSourceLogEntriesBetweenResolver = ChildResolverOf<
|
|||
QuerySourceResolver
|
||||
>;
|
||||
|
||||
export type InfraSourceLogEntryHighlightsResolver = ChildResolverOf<
|
||||
InfraResolverOf<InfraSourceResolvers.LogEntryHighlightsResolver>,
|
||||
QuerySourceResolver
|
||||
>;
|
||||
|
||||
export type InfraSourceLogSummaryBetweenResolver = ChildResolverOf<
|
||||
InfraResolverOf<InfraSourceResolvers.LogSummaryBetweenResolver>,
|
||||
QuerySourceResolver
|
||||
|
@ -49,6 +56,7 @@ export const createLogEntriesResolvers = (libs: {
|
|||
InfraSource: {
|
||||
logEntriesAround: InfraSourceLogEntriesAroundResolver;
|
||||
logEntriesBetween: InfraSourceLogEntriesBetweenResolver;
|
||||
logEntryHighlights: InfraSourceLogEntryHighlightsResolver;
|
||||
logSummaryBetween: InfraSourceLogSummaryBetweenResolver;
|
||||
logItem: InfraSourceLogItem;
|
||||
};
|
||||
|
@ -78,8 +86,7 @@ export const createLogEntriesResolvers = (libs: {
|
|||
args.key,
|
||||
countBefore + 1,
|
||||
countAfter + 1,
|
||||
parseFilterQuery(args.filterQuery),
|
||||
args.highlightQuery || undefined
|
||||
parseFilterQuery(args.filterQuery)
|
||||
);
|
||||
|
||||
const hasMoreBefore = entriesBefore.length > countBefore;
|
||||
|
@ -96,7 +103,6 @@ export const createLogEntriesResolvers = (libs: {
|
|||
hasMoreBefore,
|
||||
hasMoreAfter,
|
||||
filterQuery: args.filterQuery,
|
||||
highlightQuery: args.highlightQuery,
|
||||
entries,
|
||||
};
|
||||
},
|
||||
|
@ -106,8 +112,7 @@ export const createLogEntriesResolvers = (libs: {
|
|||
source.id,
|
||||
args.startKey,
|
||||
args.endKey,
|
||||
parseFilterQuery(args.filterQuery),
|
||||
args.highlightQuery || undefined
|
||||
parseFilterQuery(args.filterQuery)
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -116,10 +121,28 @@ export const createLogEntriesResolvers = (libs: {
|
|||
hasMoreBefore: true,
|
||||
hasMoreAfter: true,
|
||||
filterQuery: args.filterQuery,
|
||||
highlightQuery: args.highlightQuery,
|
||||
entries,
|
||||
};
|
||||
},
|
||||
async logEntryHighlights(source, args, { req }) {
|
||||
const highlightedLogEntrySets = await libs.logEntries.getLogEntryHighlights(
|
||||
req,
|
||||
source.id,
|
||||
args.startKey,
|
||||
args.endKey,
|
||||
parseHighlightInputs(args.highlights),
|
||||
parseFilterQuery(args.filterQuery)
|
||||
);
|
||||
|
||||
return highlightedLogEntrySets.map(entries => ({
|
||||
start: entries.length > 0 ? entries[0].key : null,
|
||||
end: entries.length > 0 ? entries[entries.length - 1].key : null,
|
||||
hasMoreBefore: true,
|
||||
hasMoreAfter: true,
|
||||
filterQuery: args.filterQuery,
|
||||
entries,
|
||||
}));
|
||||
},
|
||||
async logSummaryBetween(source, args, { req }) {
|
||||
UsageCollector.countLogs();
|
||||
const buckets = await libs.logEntries.getLogSummaryBucketsBetween(
|
||||
|
@ -194,3 +217,24 @@ const isConstantSegment = (
|
|||
|
||||
const isFieldSegment = (segment: InfraLogMessageSegment): segment is InfraLogMessageFieldSegment =>
|
||||
'field' in segment && 'value' in segment && 'highlights' in segment;
|
||||
|
||||
const parseHighlightInputs = (highlightInputs: InfraLogEntryHighlightInput[]) =>
|
||||
highlightInputs
|
||||
? highlightInputs.reduce<Array<{ query: JsonObject }>>(
|
||||
(parsedHighlightInputs, highlightInput) => {
|
||||
const parsedQuery = parseFilterQuery(highlightInput.query);
|
||||
if (parsedQuery) {
|
||||
return [
|
||||
...parsedHighlightInputs,
|
||||
{
|
||||
...highlightInput,
|
||||
query: parsedQuery,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return parsedHighlightInputs;
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
: [];
|
||||
|
|
|
@ -28,22 +28,30 @@ export const logEntriesSchema = gql`
|
|||
|
||||
"A special built-in column that contains the log entry's timestamp"
|
||||
type InfraLogEntryTimestampColumn {
|
||||
"The id of the corresponding column configuration"
|
||||
columnId: ID!
|
||||
"The timestamp"
|
||||
timestamp: Float!
|
||||
}
|
||||
|
||||
"A special built-in column that contains the log entry's constructed message"
|
||||
type InfraLogEntryMessageColumn {
|
||||
"The id of the corresponding column configuration"
|
||||
columnId: ID!
|
||||
"A list of the formatted log entry segments"
|
||||
message: [InfraLogMessageSegment!]!
|
||||
}
|
||||
|
||||
"A column that contains the value of a field of the log entry"
|
||||
type InfraLogEntryFieldColumn {
|
||||
"The id of the corresponding column configuration"
|
||||
columnId: ID!
|
||||
"The field name of the column"
|
||||
field: String!
|
||||
"The value of the field in the log entry"
|
||||
value: String!
|
||||
"A list of highlighted substrings of the value"
|
||||
highlights: [String!]!
|
||||
}
|
||||
|
||||
"A column of a log entry"
|
||||
|
@ -64,6 +72,10 @@ export const logEntriesSchema = gql`
|
|||
columns: [InfraLogEntryColumn!]!
|
||||
}
|
||||
|
||||
input InfraLogEntryHighlightInput {
|
||||
query: String!
|
||||
}
|
||||
|
||||
"A log summary bucket"
|
||||
type InfraLogSummaryBucket {
|
||||
"The start timestamp of the bucket"
|
||||
|
@ -133,8 +145,6 @@ export const logEntriesSchema = gql`
|
|||
countAfter: Int = 0
|
||||
"The query to filter the log entries by"
|
||||
filterQuery: String
|
||||
"The query to highlight the log entries with"
|
||||
highlightQuery: String
|
||||
): InfraLogEntryInterval!
|
||||
"A consecutive span of log entries within an interval"
|
||||
logEntriesBetween(
|
||||
|
@ -144,9 +154,18 @@ export const logEntriesSchema = gql`
|
|||
endKey: InfraTimeKeyInput!
|
||||
"The query to filter the log entries by"
|
||||
filterQuery: String
|
||||
"The query to highlight the log entries with"
|
||||
highlightQuery: String
|
||||
): InfraLogEntryInterval!
|
||||
"Sequences of log entries matching sets of highlighting queries within an interval"
|
||||
logEntryHighlights(
|
||||
"The sort key that corresponds to the start of the interval"
|
||||
startKey: InfraTimeKeyInput!
|
||||
"The sort key that corresponds to the end of the interval"
|
||||
endKey: InfraTimeKeyInput!
|
||||
"The query to filter the log entries by"
|
||||
filterQuery: String
|
||||
"The highlighting to apply to the log entries"
|
||||
highlights: [InfraLogEntryHighlightInput!]!
|
||||
): [InfraLogEntryInterval!]!
|
||||
"A consecutive span of summary buckets within an interval"
|
||||
logSummaryBetween(
|
||||
"The millisecond timestamp that corresponds to the start of the interval"
|
||||
|
|
|
@ -62,6 +62,8 @@ export interface InfraSource {
|
|||
logEntriesAround: InfraLogEntryInterval;
|
||||
/** A consecutive span of log entries within an interval */
|
||||
logEntriesBetween: InfraLogEntryInterval;
|
||||
/** Sequences of log entries matching sets of highlighting queries within an interval */
|
||||
logEntryHighlights: InfraLogEntryInterval[];
|
||||
/** A consecutive span of summary buckets within an interval */
|
||||
logSummaryBetween: InfraLogSummaryInterval;
|
||||
|
||||
|
@ -209,11 +211,15 @@ export interface InfraLogEntry {
|
|||
}
|
||||
/** A special built-in column that contains the log entry's timestamp */
|
||||
export interface InfraLogEntryTimestampColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** The timestamp */
|
||||
timestamp: number;
|
||||
}
|
||||
/** A special built-in column that contains the log entry's constructed message */
|
||||
export interface InfraLogEntryMessageColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** A list of the formatted log entry segments */
|
||||
message: InfraLogMessageSegment[];
|
||||
}
|
||||
|
@ -233,10 +239,14 @@ export interface InfraLogMessageConstantSegment {
|
|||
}
|
||||
/** A column that contains the value of a field of the log entry */
|
||||
export interface InfraLogEntryFieldColumn {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId: string;
|
||||
/** The field name of the column */
|
||||
field: string;
|
||||
/** The value of the field in the log entry */
|
||||
value: string;
|
||||
/** A list of highlighted substrings of the value */
|
||||
highlights: string[];
|
||||
}
|
||||
/** A consecutive sequence of log summary buckets */
|
||||
export interface InfraLogSummaryInterval {
|
||||
|
@ -353,6 +363,10 @@ export interface InfraTimeKeyInput {
|
|||
tiebreaker: number;
|
||||
}
|
||||
|
||||
export interface InfraLogEntryHighlightInput {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface InfraTimerangeInput {
|
||||
/** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
|
||||
interval: string;
|
||||
|
@ -447,8 +461,6 @@ export interface LogEntriesAroundInfraSourceArgs {
|
|||
countAfter?: number | null;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The query to highlight the log entries with */
|
||||
highlightQuery?: string | null;
|
||||
}
|
||||
export interface LogEntriesBetweenInfraSourceArgs {
|
||||
/** The sort key that corresponds to the start of the interval */
|
||||
|
@ -457,8 +469,16 @@ export interface LogEntriesBetweenInfraSourceArgs {
|
|||
endKey: InfraTimeKeyInput;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The query to highlight the log entries with */
|
||||
highlightQuery?: string | null;
|
||||
}
|
||||
export interface LogEntryHighlightsInfraSourceArgs {
|
||||
/** The sort key that corresponds to the start of the interval */
|
||||
startKey: InfraTimeKeyInput;
|
||||
/** The sort key that corresponds to the end of the interval */
|
||||
endKey: InfraTimeKeyInput;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The highlighting to apply to the log entries */
|
||||
highlights: InfraLogEntryHighlightInput[];
|
||||
}
|
||||
export interface LogSummaryBetweenInfraSourceArgs {
|
||||
/** The millisecond timestamp that corresponds to the start of the interval */
|
||||
|
@ -643,6 +663,8 @@ export namespace InfraSourceResolvers {
|
|||
logEntriesAround?: LogEntriesAroundResolver<InfraLogEntryInterval, TypeParent, Context>;
|
||||
/** A consecutive span of log entries within an interval */
|
||||
logEntriesBetween?: LogEntriesBetweenResolver<InfraLogEntryInterval, TypeParent, Context>;
|
||||
/** Sequences of log entries matching sets of highlighting queries within an interval */
|
||||
logEntryHighlights?: LogEntryHighlightsResolver<InfraLogEntryInterval[], TypeParent, Context>;
|
||||
/** A consecutive span of summary buckets within an interval */
|
||||
logSummaryBetween?: LogSummaryBetweenResolver<InfraLogSummaryInterval, TypeParent, Context>;
|
||||
|
||||
|
@ -708,8 +730,6 @@ export namespace InfraSourceResolvers {
|
|||
countAfter?: number | null;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The query to highlight the log entries with */
|
||||
highlightQuery?: string | null;
|
||||
}
|
||||
|
||||
export type LogEntriesBetweenResolver<
|
||||
|
@ -724,8 +744,22 @@ export namespace InfraSourceResolvers {
|
|||
endKey: InfraTimeKeyInput;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The query to highlight the log entries with */
|
||||
highlightQuery?: string | null;
|
||||
}
|
||||
|
||||
export type LogEntryHighlightsResolver<
|
||||
R = InfraLogEntryInterval[],
|
||||
Parent = InfraSource,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context, LogEntryHighlightsArgs>;
|
||||
export interface LogEntryHighlightsArgs {
|
||||
/** The sort key that corresponds to the start of the interval */
|
||||
startKey: InfraTimeKeyInput;
|
||||
/** The sort key that corresponds to the end of the interval */
|
||||
endKey: InfraTimeKeyInput;
|
||||
/** The query to filter the log entries by */
|
||||
filterQuery?: string | null;
|
||||
/** The highlighting to apply to the log entries */
|
||||
highlights: InfraLogEntryHighlightInput[];
|
||||
}
|
||||
|
||||
export type LogSummaryBetweenResolver<
|
||||
|
@ -1223,10 +1257,17 @@ export namespace InfraLogEntryResolvers {
|
|||
/** A special built-in column that contains the log entry's timestamp */
|
||||
export namespace InfraLogEntryTimestampColumnResolvers {
|
||||
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogEntryTimestampColumn> {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId?: ColumnIdResolver<string, TypeParent, Context>;
|
||||
/** The timestamp */
|
||||
timestamp?: TimestampResolver<number, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type ColumnIdResolver<
|
||||
R = string,
|
||||
Parent = InfraLogEntryTimestampColumn,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type TimestampResolver<
|
||||
R = number,
|
||||
Parent = InfraLogEntryTimestampColumn,
|
||||
|
@ -1236,10 +1277,17 @@ export namespace InfraLogEntryTimestampColumnResolvers {
|
|||
/** A special built-in column that contains the log entry's constructed message */
|
||||
export namespace InfraLogEntryMessageColumnResolvers {
|
||||
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogEntryMessageColumn> {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId?: ColumnIdResolver<string, TypeParent, Context>;
|
||||
/** A list of the formatted log entry segments */
|
||||
message?: MessageResolver<InfraLogMessageSegment[], TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type ColumnIdResolver<
|
||||
R = string,
|
||||
Parent = InfraLogEntryMessageColumn,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type MessageResolver<
|
||||
R = InfraLogMessageSegment[],
|
||||
Parent = InfraLogEntryMessageColumn,
|
||||
|
@ -1289,12 +1337,21 @@ export namespace InfraLogMessageConstantSegmentResolvers {
|
|||
/** A column that contains the value of a field of the log entry */
|
||||
export namespace InfraLogEntryFieldColumnResolvers {
|
||||
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogEntryFieldColumn> {
|
||||
/** The id of the corresponding column configuration */
|
||||
columnId?: ColumnIdResolver<string, TypeParent, Context>;
|
||||
/** The field name of the column */
|
||||
field?: FieldResolver<string, TypeParent, Context>;
|
||||
/** The value of the field in the log entry */
|
||||
value?: ValueResolver<string, TypeParent, Context>;
|
||||
/** A list of highlighted substrings of the value */
|
||||
highlights?: HighlightsResolver<string[], TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type ColumnIdResolver<
|
||||
R = string,
|
||||
Parent = InfraLogEntryFieldColumn,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type FieldResolver<
|
||||
R = string,
|
||||
Parent = InfraLogEntryFieldColumn,
|
||||
|
@ -1305,6 +1362,11 @@ export namespace InfraLogEntryFieldColumnResolvers {
|
|||
Parent = InfraLogEntryFieldColumn,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type HighlightsResolver<
|
||||
R = string[],
|
||||
Parent = InfraLogEntryFieldColumn,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
/** A consecutive sequence of log summary buckets */
|
||||
export namespace InfraLogSummaryIntervalResolvers {
|
||||
|
|
|
@ -48,7 +48,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
|
|||
direction: 'asc' | 'desc',
|
||||
maxCount: number,
|
||||
filterQuery: LogEntryQuery,
|
||||
highlightQuery: string
|
||||
highlightQuery?: LogEntryQuery
|
||||
): Promise<LogEntryDocument[]> {
|
||||
if (maxCount <= 0) {
|
||||
return [];
|
||||
|
@ -87,7 +87,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
|
|||
start: TimeKey,
|
||||
end: TimeKey,
|
||||
filterQuery: LogEntryQuery,
|
||||
highlightQuery: string
|
||||
highlightQuery?: LogEntryQuery
|
||||
): Promise<LogEntryDocument[]> {
|
||||
const documents = await this.getLogEntryDocumentsBetween(
|
||||
request,
|
||||
|
@ -203,7 +203,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
|
|||
after: TimeKey | null,
|
||||
maxCount: number,
|
||||
filterQuery?: LogEntryQuery,
|
||||
highlightQuery?: string
|
||||
highlightQuery?: LogEntryQuery
|
||||
): Promise<LogEntryDocument[]> {
|
||||
if (maxCount <= 0) {
|
||||
return [];
|
||||
|
@ -236,6 +236,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
|
|||
number_of_fragments: 100,
|
||||
post_tags: [''],
|
||||
pre_tags: [''],
|
||||
highlight_query: highlightQuery,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
@ -314,6 +315,7 @@ const convertHitToLogEntryDocument = (fields: string[]) => (
|
|||
: flattenedFields,
|
||||
{} as { [fieldName: string]: string | number | boolean | null }
|
||||
),
|
||||
highlights: hit.highlight || {},
|
||||
key: {
|
||||
time: hit.sort[0],
|
||||
tiebreaker: hit.sort[1],
|
||||
|
|
|
@ -41,8 +41,11 @@ describe('Filebeat Rules', () => {
|
|||
'user_agent.os.minor': '12',
|
||||
'user_agent.os.name': 'Mac OS X',
|
||||
};
|
||||
const highlights = {
|
||||
'http.request.method': ['GET'],
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, highlights)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -73,7 +76,9 @@ Array [
|
|||
},
|
||||
Object {
|
||||
"field": "http.request.method",
|
||||
"highlights": Array [],
|
||||
"highlights": Array [
|
||||
"GET",
|
||||
],
|
||||
"value": "GET",
|
||||
},
|
||||
Object {
|
||||
|
@ -128,7 +133,7 @@ Array [
|
|||
'source.ip': '192.168.33.1',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[apache][",
|
||||
|
@ -164,7 +169,7 @@ Array [
|
|||
'apache2.access.body_sent.bytes': 1024,
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[apache][access] ",
|
||||
|
@ -233,7 +238,7 @@ Array [
|
|||
'apache2.error.level': 'notice',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[apache][",
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('Filebeat Rules', () => {
|
|||
user: { 'audit.id': '4294967295', id: '0', 'saved.id': '74' },
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[AuditD][",
|
||||
|
@ -155,7 +155,7 @@ Array [
|
|||
},
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[AuditD][",
|
||||
|
@ -238,7 +238,7 @@ Array [
|
|||
'input.type': 'log',
|
||||
'log.offset': 0,
|
||||
};
|
||||
const message = format(event);
|
||||
const message = format(event, {});
|
||||
expect(message).toEqual([
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type', highlights: [], value: 'MAC_IPSEC_EVENT' },
|
||||
|
@ -287,7 +287,7 @@ Array [
|
|||
'input.type': 'log',
|
||||
'log.offset': 174,
|
||||
};
|
||||
const message = format(event);
|
||||
const message = format(event, {});
|
||||
expect(message).toEqual([
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type', highlights: [], value: 'SYSCALL' },
|
||||
|
@ -323,7 +323,7 @@ Array [
|
|||
'input.type': 'log',
|
||||
'log.offset': 174,
|
||||
};
|
||||
const message = format(event);
|
||||
const message = format(event, {});
|
||||
expect(message).toEqual([
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type', highlights: [], value: 'EXAMPLE' },
|
||||
|
@ -348,7 +348,7 @@ Array [
|
|||
'input.type': 'log',
|
||||
'log.offset': 174,
|
||||
};
|
||||
const message = format(event);
|
||||
const message = format(event, {});
|
||||
expect(message).toEqual([
|
||||
{ constant: '[AuditD][' },
|
||||
{ field: 'auditd.log.record_type', highlights: [], value: 'EXAMPLE' },
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('Filebeat Rules', () => {
|
|||
'source.port': 40780,
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[HAProxy] ",
|
||||
|
@ -98,7 +98,7 @@ Array [
|
|||
'source.port': 40962,
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[HAProxy][tcp] ",
|
||||
|
@ -245,7 +245,7 @@ Array [
|
|||
'source.port': 38862,
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[HAProxy][http] ",
|
||||
|
@ -428,7 +428,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[HAProxy] ",
|
||||
|
@ -488,7 +488,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[HAProxy][tcp] ",
|
||||
|
@ -630,7 +630,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[HAProxy][http] ",
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('Filebeat Rules', () => {
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[Icinga][",
|
||||
|
@ -71,7 +71,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[Icinga][",
|
||||
|
@ -114,7 +114,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[Icinga][",
|
||||
|
|
|
@ -59,7 +59,7 @@ describe('Filebeat Rules', () => {
|
|||
'user_agent.version': '70.0.3538',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -168,7 +168,7 @@ Array [
|
|||
'user_agent.version': '7.0',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -270,7 +270,7 @@ Array [
|
|||
'url.original': '/qos/1kbfile.txt',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[iis][error] ",
|
||||
|
@ -332,7 +332,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[iis][access] ",
|
||||
|
@ -422,7 +422,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[iis][access] ",
|
||||
|
@ -505,7 +505,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[iis][error] ",
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('Filebeat Rules', () => {
|
|||
'service.type': 'kafka',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('Filebeat Rules', () => {
|
|||
'service.type': 'logstash',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -81,7 +81,7 @@ Array [
|
|||
'service.type': 'logstash',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[Logstash][",
|
||||
|
@ -120,7 +120,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[Logstash][",
|
||||
|
@ -173,7 +173,7 @@ Array [
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[Logstash][",
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('Filebeat Rules', () => {
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[MongoDB][",
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('Filebeat Rules', () => {
|
|||
'service.type': 'mysql',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -81,7 +81,7 @@ Array [
|
|||
'user.name': 'appuser',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[MySQL][slowlog] ",
|
||||
|
@ -147,7 +147,7 @@ Array [
|
|||
'mysql.error.message':
|
||||
"Access denied for user 'petclinicdd'@'47.153.152.234' (using password: YES)",
|
||||
};
|
||||
const message = format(errorDoc);
|
||||
const message = format(errorDoc, {});
|
||||
expect(message).toEqual([
|
||||
{
|
||||
constant: '[MySQL][error] ',
|
||||
|
@ -168,7 +168,7 @@ Array [
|
|||
'mysql.slowlog.ip': '192.168.1.42',
|
||||
'mysql.slowlog.host': 'webserver-01',
|
||||
};
|
||||
const message = format(errorDoc);
|
||||
const message = format(errorDoc, {});
|
||||
expect(message).toEqual([
|
||||
{
|
||||
constant: '[MySQL][slowlog] ',
|
||||
|
|
|
@ -40,7 +40,7 @@ describe('Filebeat Rules', () => {
|
|||
'user_agent.patch': 'a2',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -128,7 +128,7 @@ Array [
|
|||
'service.type': 'nginx',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[nginx]",
|
||||
|
@ -167,7 +167,7 @@ Array [
|
|||
'nginx.access.response_code': 200,
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[nginx][access] ",
|
||||
|
@ -236,7 +236,7 @@ Array [
|
|||
'nginx.error.level': 'error',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[nginx]",
|
||||
|
|
|
@ -44,7 +44,7 @@ describe('Filebeat Rules', () => {
|
|||
'prospector.type': 'log',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[Osquery][",
|
||||
|
|
|
@ -51,7 +51,7 @@ describe('Filebeat Rules', () => {
|
|||
'traefik.access.user_name': '-',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[traefik][access] ",
|
||||
|
|
|
@ -20,8 +20,11 @@ describe('Generic Rules', () => {
|
|||
'log.level': 'TEST_LEVEL',
|
||||
first_generic_message: 'TEST_MESSAGE',
|
||||
};
|
||||
const highlights = {
|
||||
first_generic_message: ['TEST'],
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, highlights)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -44,7 +47,9 @@ Array [
|
|||
},
|
||||
Object {
|
||||
"field": "first_generic_message",
|
||||
"highlights": Array [],
|
||||
"highlights": Array [
|
||||
"TEST",
|
||||
],
|
||||
"value": "TEST_MESSAGE",
|
||||
},
|
||||
]
|
||||
|
@ -58,7 +63,7 @@ Array [
|
|||
first_generic_message: 'TEST_MESSAGE',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -86,7 +91,7 @@ Array [
|
|||
first_generic_message: 'FIRST_TEST_MESSAGE',
|
||||
};
|
||||
|
||||
expect(format(firstFlattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(firstFlattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"field": "first_generic_message",
|
||||
|
@ -101,7 +106,7 @@ Array [
|
|||
second_generic_message: 'SECOND_TEST_MESSAGE',
|
||||
};
|
||||
|
||||
expect(format(secondFlattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(secondFlattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"field": "second_generic_message",
|
||||
|
@ -121,7 +126,7 @@ Array [
|
|||
'log.original': 'TEST_MESSAGE',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"constant": "[",
|
||||
|
@ -149,7 +154,7 @@ Array [
|
|||
'log.original': 'TEST_MESSAGE',
|
||||
};
|
||||
|
||||
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
|
||||
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"field": "log.original",
|
||||
|
|
|
@ -25,7 +25,12 @@ import {
|
|||
} from '../../sources';
|
||||
import { getBuiltinRules } from './builtin_rules';
|
||||
import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields';
|
||||
import { compileFormattingRules, CompiledLogMessageFormattingRule } from './message';
|
||||
import {
|
||||
CompiledLogMessageFormattingRule,
|
||||
Fields,
|
||||
Highlights,
|
||||
compileFormattingRules,
|
||||
} from './message';
|
||||
|
||||
export class InfraLogEntriesDomain {
|
||||
constructor(
|
||||
|
@ -40,7 +45,7 @@ export class InfraLogEntriesDomain {
|
|||
maxCountBefore: number,
|
||||
maxCountAfter: number,
|
||||
filterQuery?: LogEntryQuery,
|
||||
highlightQuery?: string
|
||||
highlightQuery?: LogEntryQuery
|
||||
): Promise<{ entriesBefore: InfraLogEntry[]; entriesAfter: InfraLogEntry[] }> {
|
||||
if (maxCountBefore <= 0 && maxCountAfter <= 0) {
|
||||
return {
|
||||
|
@ -100,7 +105,7 @@ export class InfraLogEntriesDomain {
|
|||
startKey: TimeKey,
|
||||
endKey: TimeKey,
|
||||
filterQuery?: LogEntryQuery,
|
||||
highlightQuery?: string
|
||||
highlightQuery?: LogEntryQuery
|
||||
): Promise<InfraLogEntry[]> {
|
||||
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
|
||||
const messageFormattingRules = compileFormattingRules(
|
||||
|
@ -122,6 +127,55 @@ export class InfraLogEntriesDomain {
|
|||
return entries;
|
||||
}
|
||||
|
||||
public async getLogEntryHighlights(
|
||||
request: InfraFrameworkRequest,
|
||||
sourceId: string,
|
||||
startKey: TimeKey,
|
||||
endKey: TimeKey,
|
||||
highlights: Array<{
|
||||
query: JsonObject;
|
||||
}>,
|
||||
filterQuery?: LogEntryQuery
|
||||
): Promise<InfraLogEntry[][]> {
|
||||
const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId);
|
||||
const messageFormattingRules = compileFormattingRules(
|
||||
getBuiltinRules(configuration.fields.message)
|
||||
);
|
||||
const requiredFields = getRequiredFields(configuration, messageFormattingRules);
|
||||
|
||||
const documentSets = await Promise.all(
|
||||
highlights.map(async highlight => {
|
||||
const query = filterQuery
|
||||
? {
|
||||
bool: {
|
||||
must: [filterQuery, highlight.query],
|
||||
},
|
||||
}
|
||||
: highlight.query;
|
||||
const documents = await this.adapter.getContainedLogEntryDocuments(
|
||||
request,
|
||||
configuration,
|
||||
requiredFields,
|
||||
startKey,
|
||||
endKey,
|
||||
query,
|
||||
highlight.query
|
||||
);
|
||||
const entries = documents.map(
|
||||
convertLogDocumentToEntry(
|
||||
sourceId,
|
||||
configuration.logColumns,
|
||||
messageFormattingRules.format
|
||||
)
|
||||
);
|
||||
|
||||
return entries;
|
||||
})
|
||||
);
|
||||
|
||||
return documentSets;
|
||||
}
|
||||
|
||||
public async getLogSummaryBucketsBetween(
|
||||
request: InfraFrameworkRequest,
|
||||
sourceId: string,
|
||||
|
@ -185,7 +239,7 @@ export interface LogEntriesAdapter {
|
|||
direction: 'asc' | 'desc',
|
||||
maxCount: number,
|
||||
filterQuery?: LogEntryQuery,
|
||||
highlightQuery?: string
|
||||
highlightQuery?: LogEntryQuery
|
||||
): Promise<LogEntryDocument[]>;
|
||||
|
||||
getContainedLogEntryDocuments(
|
||||
|
@ -195,7 +249,7 @@ export interface LogEntriesAdapter {
|
|||
start: TimeKey,
|
||||
end: TimeKey,
|
||||
filterQuery?: LogEntryQuery,
|
||||
highlightQuery?: string
|
||||
highlightQuery?: LogEntryQuery
|
||||
): Promise<LogEntryDocument[]>;
|
||||
|
||||
getContainedLogSummaryBuckets(
|
||||
|
@ -217,19 +271,16 @@ export interface LogEntriesAdapter {
|
|||
export type LogEntryQuery = JsonObject;
|
||||
|
||||
export interface LogEntryDocument {
|
||||
fields: LogEntryDocumentFields;
|
||||
fields: Fields;
|
||||
gid: string;
|
||||
highlights: Highlights;
|
||||
key: TimeKey;
|
||||
}
|
||||
|
||||
export interface LogEntryDocumentFields {
|
||||
[fieldName: string]: string | number | boolean | null;
|
||||
}
|
||||
|
||||
const convertLogDocumentToEntry = (
|
||||
sourceId: string,
|
||||
logColumns: InfraSourceConfiguration['logColumns'],
|
||||
formatLogMessage: (fields: LogEntryDocumentFields) => InfraLogMessageSegment[]
|
||||
formatLogMessage: (fields: Fields, highlights: Highlights) => InfraLogMessageSegment[]
|
||||
) => (document: LogEntryDocument): InfraLogEntry => ({
|
||||
key: document.key,
|
||||
gid: document.gid,
|
||||
|
@ -237,15 +288,19 @@ const convertLogDocumentToEntry = (
|
|||
columns: logColumns.map(logColumn => {
|
||||
if (SavedSourceConfigurationTimestampColumnRuntimeType.is(logColumn)) {
|
||||
return {
|
||||
columnId: logColumn.timestampColumn.id,
|
||||
timestamp: document.key.time,
|
||||
};
|
||||
} else if (SavedSourceConfigurationMessageColumnRuntimeType.is(logColumn)) {
|
||||
return {
|
||||
message: formatLogMessage(document.fields),
|
||||
columnId: logColumn.messageColumn.id,
|
||||
message: formatLogMessage(document.fields, document.highlights),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
columnId: logColumn.fieldColumn.id,
|
||||
field: logColumn.fieldColumn.field,
|
||||
highlights: document.highlights[logColumn.fieldColumn.field] || [],
|
||||
value: stringify(document.fields[logColumn.fieldColumn.field] || null),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -30,10 +30,10 @@ export function compileFormattingRules(
|
|||
)
|
||||
)
|
||||
),
|
||||
format(fields): InfraLogMessageSegment[] {
|
||||
format(fields, highlights): InfraLogMessageSegment[] {
|
||||
for (const compiledRule of compiledRules) {
|
||||
if (compiledRule.fulfillsCondition(fields)) {
|
||||
return compiledRule.format(fields);
|
||||
return compiledRule.format(fields, highlights);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ const compileCondition = (
|
|||
|
||||
const catchAllCondition: CompiledLogMessageFormattingCondition = {
|
||||
conditionFields: [] as string[],
|
||||
fulfillsCondition: (fields: Fields) => false,
|
||||
fulfillsCondition: () => false,
|
||||
};
|
||||
|
||||
const compileExistsCondition = (condition: LogMessageFormattingCondition) =>
|
||||
|
@ -101,15 +101,15 @@ const compileFormattingInstructions = (
|
|||
...combinedFormattingInstructions.formattingFields,
|
||||
...compiledFormattingInstruction.formattingFields,
|
||||
],
|
||||
format: (fields: Fields) => [
|
||||
...combinedFormattingInstructions.format(fields),
|
||||
...compiledFormattingInstruction.format(fields),
|
||||
format: (fields: Fields, highlights: Highlights) => [
|
||||
...combinedFormattingInstructions.format(fields, highlights),
|
||||
...compiledFormattingInstruction.format(fields, highlights),
|
||||
],
|
||||
};
|
||||
},
|
||||
{
|
||||
formattingFields: [],
|
||||
format: (fields: Fields) => [],
|
||||
format: () => [],
|
||||
} as CompiledLogMessageFormattingInstruction
|
||||
);
|
||||
|
||||
|
@ -124,7 +124,7 @@ const compileFormattingInstruction = (
|
|||
|
||||
const catchAllFormattingInstruction: CompiledLogMessageFormattingInstruction = {
|
||||
formattingFields: [],
|
||||
format: (fields: Fields) => [
|
||||
format: () => [
|
||||
{
|
||||
constant: 'invalid format',
|
||||
},
|
||||
|
@ -137,13 +137,14 @@ const compileFieldReferenceFormattingInstruction = (
|
|||
'field' in formattingInstruction
|
||||
? {
|
||||
formattingFields: [formattingInstruction.field],
|
||||
format: (fields: Fields) => {
|
||||
format: (fields, highlights) => {
|
||||
const value = fields[formattingInstruction.field];
|
||||
const highlightedValues = highlights[formattingInstruction.field];
|
||||
return [
|
||||
{
|
||||
field: formattingInstruction.field,
|
||||
value: typeof value === 'object' ? stringify(value) : `${value}`,
|
||||
highlights: [],
|
||||
highlights: highlightedValues || [],
|
||||
},
|
||||
];
|
||||
},
|
||||
|
@ -156,7 +157,7 @@ const compileConstantFormattingInstruction = (
|
|||
'constant' in formattingInstruction
|
||||
? {
|
||||
formattingFields: [] as string[],
|
||||
format: (fields: Fields) => [
|
||||
format: () => [
|
||||
{
|
||||
constant: formattingInstruction.constant,
|
||||
},
|
||||
|
@ -164,14 +165,18 @@ const compileConstantFormattingInstruction = (
|
|||
}
|
||||
: null;
|
||||
|
||||
interface Fields {
|
||||
export interface Fields {
|
||||
[fieldName: string]: string | number | object | boolean | null;
|
||||
}
|
||||
|
||||
export interface Highlights {
|
||||
[fieldName: string]: string[];
|
||||
}
|
||||
|
||||
export interface CompiledLogMessageFormattingRule {
|
||||
requiredFields: string[];
|
||||
fulfillsCondition(fields: Fields): boolean;
|
||||
format(fields: Fields): InfraLogMessageSegment[];
|
||||
format(fields: Fields, highlights: Highlights): InfraLogMessageSegment[];
|
||||
}
|
||||
|
||||
export interface CompiledLogMessageFormattingCondition {
|
||||
|
@ -181,5 +186,5 @@ export interface CompiledLogMessageFormattingCondition {
|
|||
|
||||
export interface CompiledLogMessageFormattingInstruction {
|
||||
formattingFields: string[];
|
||||
format(fields: Fields): InfraLogMessageSegment[];
|
||||
format(fields: Fields, highlights: Highlights): InfraLogMessageSegment[];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue