[7.x] [Logs UI] Add single phrase highlighting (#39569) (#40221)

Backports the following commits to 7.x:
 - [Logs UI] Add single phrase highlighting  (#39569)
This commit is contained in:
Felix Stürmer 2019-07-03 01:55:23 +02:00 committed by GitHub
parent 2f63d5fed5
commit f68b5a1edb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1551 additions and 286 deletions

View file

@ -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
}
}
}
`,
};

View file

@ -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[];
};
}

View file

@ -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,
});

View file

@ -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))

View file

@ -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;
`;

View file

@ -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"
>

View file

@ -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]
);

View file

@ -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) {

View file

@ -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`,
},
];
);

View file

@ -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');
});

View file

@ -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}

View file

@ -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;
}

View file

@ -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}

View file

@ -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;

View file

@ -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

View file

@ -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,

View file

@ -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';

View file

@ -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}
`;

View file

@ -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);

View file

@ -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} />
</>
);

View file

@ -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),
}),

View file

@ -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,
});
/**

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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",

View file

@ -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[];
};
}

View file

@ -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 />

View file

@ -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>
);
};

View file

@ -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>
{({

View file

@ -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);
}
}

View file

@ -5,3 +5,4 @@
*/
export * from './log_entry';
export * from './log_entry_highlight';

View file

@ -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;

View file

@ -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);
};

View file

@ -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);

View file

@ -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]
);
};

View file

@ -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;
}
},
[]
)
: [];

View file

@ -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"

View file

@ -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 {

View file

@ -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],

View file

@ -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][",

View file

@ -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' },

View file

@ -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] ",

View file

@ -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][",

View file

@ -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] ",

View file

@ -27,7 +27,7 @@ describe('Filebeat Rules', () => {
'service.type': 'kafka',
};
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
Array [
Object {
"constant": "[",

View file

@ -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][",

View file

@ -27,7 +27,7 @@ describe('Filebeat Rules', () => {
'prospector.type': 'log',
};
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
Array [
Object {
"constant": "[MongoDB][",

View file

@ -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] ',

View file

@ -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]",

View file

@ -44,7 +44,7 @@ describe('Filebeat Rules', () => {
'prospector.type': 'log',
};
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
Array [
Object {
"constant": "[Osquery][",

View file

@ -51,7 +51,7 @@ describe('Filebeat Rules', () => {
'traefik.access.user_name': '-',
};
expect(format(flattenedDocument)).toMatchInlineSnapshot(`
expect(format(flattenedDocument, {})).toMatchInlineSnapshot(`
Array [
Object {
"constant": "[traefik][access] ",

View file

@ -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",

View file

@ -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),
};
}

View file

@ -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[];
}