[Log UI] Flyout for Log Events (#28885)

* Adding flyout to log viewer

* Adding filtering

* Fixing typescript errors

* Adding a test for graphql; fixing test data for 7.0.0

* Adding terminate_after:1 to logItem request

* fixing test data

* Switching back to old data

* Fixing data for tests

* Adding i18n translations

* changing label from add to set

* Make flyout call more robust; fixing typings

* Adding loading screen to flyout

* Fixing linting errors

* Update x-pack/plugins/infra/public/components/logging/log_flyout.tsx

Co-Authored-By: simianhacker <chris@chriscowan.us>

* Fixing visible mis-spelling

* Fixing types

* Change withLogFlyout to be conditional; Add icon instead of onClick for flyout

* Adding dark mode support

* Adding user-select:none to icon div

* Removing remnants of a failed experiment

* Adding aria-label to view details button

* Fixing padding on date element

* Removing unused variable that somehow got past the linters

* Fixing empty_kibana

* Fixing data for infra

* Fixing merge weirdness
This commit is contained in:
Chris Cowan 2019-01-31 08:34:09 -07:00 committed by GitHub
parent caa31170d3
commit 7e9315a528
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1171 additions and 57 deletions

View file

@ -0,0 +1,114 @@
/*
* 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 {
EuiBasicTable,
EuiButtonIcon,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiToolTip,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import styled from 'styled-components';
import { InfraLogItem, InfraLogItemField } from '../../graphql/types';
import { InfraLoadingPanel } from '../loading';
interface Props {
flyoutItem: InfraLogItem | null;
hideFlyout: () => void;
setFilter: (filter: string) => void;
intl: InjectedIntl;
loading: boolean;
}
export const LogFlyout = injectI18n(
({ flyoutItem, loading, hideFlyout, setFilter, intl }: Props) => {
const handleFilter = (field: InfraLogItemField) => () => {
const filter = `${field.field}:"${field.value}"`;
setFilter(filter);
};
const columns = [
{
field: 'field',
name: intl.formatMessage({
defaultMessage: 'Field',
id: 'xpack.infra.logFlyout.fieldColumnLabel',
}),
sortable: true,
},
{
field: 'value',
name: intl.formatMessage({
defaultMessage: 'Value',
id: 'xpack.infra.logFlyout.valueColumnLabel',
}),
sortable: true,
render: (name: string, item: InfraLogItemField) => (
<span>
<EuiToolTip
content={intl.formatMessage({
id: 'xpack.infra.logFlyout.setFilterTooltip',
defaultMessage: 'Set Filter',
})}
>
<EuiButtonIcon
color="text"
iconType="filter"
aria-label={intl.formatMessage({
id: 'xpack.infra.logFlyout.filterAriaLabel',
defaultMessage: 'Filter',
})}
onClick={handleFilter(item)}
/>
</EuiToolTip>
{item.value}
</span>
),
},
];
return (
<EuiFlyout onClose={() => hideFlyout()} size="m">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h3 id="flyoutTitle">
<FormattedMessage
defaultMessage="Log event document details"
id="xpack.infra.logFlyout.flyoutTitle"
/>
</h3>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{loading || flyoutItem === null ? (
<InfraFlyoutLoadingPanel>
<InfraLoadingPanel
height="100%"
width="100%"
text={intl.formatMessage({
id: 'xpack.infra.logFlyout.loadingMessage',
defaultMessage: 'Loading Event',
})}
/>
</InfraFlyoutLoadingPanel>
) : (
<EuiBasicTable columns={columns} items={flyoutItem.fields} />
)}
</EuiFlyoutBody>
</EuiFlyout>
);
}
);
export const InfraFlyoutLoadingPanel = styled.div`
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
`;

View file

@ -64,6 +64,7 @@ const LogTextStreamItemDateFieldWrapper = LogTextStreamItemField.extend.attrs<{
border-right: solid 2px ${props => props.theme.eui.euiColorLightShade};
color: ${props => props.theme.eui.euiColorDarkShade};
white-space: pre;
padding: 0 ${props => props.theme.eui.paddingSizes.l};
${props => (props.hasHighlights ? highlightedFieldStyle : '')};
${props => (props.isHovered ? hoveredFieldStyle : '')};

View file

@ -19,5 +19,5 @@ export const LogTextStreamItemField = styled.div.attrs<{
[switchProp.default]: props.theme.eui.euiFontSize,
})};
line-height: ${props => props.theme.eui.euiLineHeight};
padding: 2px ${props => props.theme.eui.euiSize};
padding: 2px ${props => props.theme.eui.euiSize} 2px 0;
`;

View file

@ -105,7 +105,6 @@ const LogTextStreamItemMessageFieldWrapper = LogTextStreamItemField.extend.attrs
const HighlightSpan = styled.span`
display: inline-block;
padding: 0 ${props => props.theme.eui.euiSizeXs};
background-color: ${props => props.theme.eui.euiColorSecondary};
color: ${props => props.theme.eui.euiColorGhost};
font-weight: ${props => props.theme.eui.euiFontWeightMedium};

View file

@ -14,10 +14,11 @@ interface StreamItemProps {
item: StreamItem;
scale: TextScale;
wrap: boolean;
openFlyoutWithItem: (id: string) => void;
}
export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
({ item, scale, wrap }, ref) => {
({ item, scale, wrap, openFlyoutWithItem }, ref) => {
switch (item.kind) {
case 'logEntry':
return (
@ -27,6 +28,7 @@ export const LogTextStreamItemView = React.forwardRef<Element, StreamItemProps>(
searchResult={item.searchResult}
scale={scale}
wrap={wrap}
openFlyoutWithItem={openFlyoutWithItem}
/>
);
}

View file

@ -4,9 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { darken, transparentize } from 'polished';
import * as React from 'react';
import styled from 'styled-components';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { LogEntry } from '../../../../common/log_entry';
import { SearchResult } from '../../../../common/log_search_result';
import { TextScale } from '../../../../common/log_text_scale';
@ -20,65 +23,110 @@ interface LogTextStreamLogEntryItemViewProps {
searchResult?: SearchResult;
scale: TextScale;
wrap: boolean;
openFlyoutWithItem: (id: string) => void;
intl: InjectedIntl;
}
interface LogTextStreamLogEntryItemViewState {
isHovered: boolean;
}
export class LogTextStreamLogEntryItemView extends React.PureComponent<
LogTextStreamLogEntryItemViewProps,
LogTextStreamLogEntryItemViewState
> {
public readonly state = {
isHovered: false,
};
public handleMouseEnter: React.MouseEventHandler<HTMLDivElement> = () => {
this.setState({
isHovered: true,
});
};
public handleMouseLeave: React.MouseEventHandler<HTMLDivElement> = () => {
this.setState({
export const LogTextStreamLogEntryItemView = injectI18n(
class extends React.PureComponent<
LogTextStreamLogEntryItemViewProps,
LogTextStreamLogEntryItemViewState
> {
public readonly state = {
isHovered: false,
});
};
};
public render() {
const { boundingBoxRef, logEntry, scale, searchResult, wrap } = this.props;
const { isHovered } = this.state;
public handleMouseEnter: React.MouseEventHandler<HTMLDivElement> = () => {
this.setState({
isHovered: true,
});
};
return (
<LogTextStreamLogEntryItemDiv
innerRef={
/* Workaround for missing RefObject support in styled-components */
boundingBoxRef as any
}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<LogTextStreamItemDateField
hasHighlights={!!searchResult}
isHovered={isHovered}
scale={scale}
public handleMouseLeave: React.MouseEventHandler<HTMLDivElement> = () => {
this.setState({
isHovered: false,
});
};
public handleClick: React.MouseEventHandler<HTMLButtonElement> = () => {
this.props.openFlyoutWithItem(this.props.logEntry.gid);
};
public render() {
const { intl, boundingBoxRef, logEntry, scale, searchResult, wrap } = this.props;
const { isHovered } = this.state;
const viewDetailsLabel = intl.formatMessage({
id: 'xpack.infra.logEntryItemView.viewDetailsToolTip',
defaultMessage: 'View Details',
});
return (
<LogTextStreamLogEntryItemDiv
innerRef={
/* Workaround for missing RefObject support in styled-components */
boundingBoxRef as any
}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{formatTime(logEntry.fields.time)}
</LogTextStreamItemDateField>
<LogTextStreamItemMessageField
highlights={searchResult ? searchResult.matches.message || [] : []}
isHovered={isHovered}
isWrapped={wrap}
scale={scale}
>
{logEntry.fields.message}
</LogTextStreamItemMessageField>
</LogTextStreamLogEntryItemDiv>
);
<LogTextStreamItemDateField
hasHighlights={!!searchResult}
isHovered={isHovered}
scale={scale}
>
{formatTime(logEntry.fields.time)}
</LogTextStreamItemDateField>
<LogTextStreamIconDiv isHovered={isHovered}>
{isHovered ? (
<EuiToolTip content={viewDetailsLabel}>
<EuiButtonIcon
onClick={this.handleClick}
iconType="expand"
aria-label={viewDetailsLabel}
/>
</EuiToolTip>
) : (
<EmptyIcon />
)}
</LogTextStreamIconDiv>
<LogTextStreamItemMessageField
highlights={searchResult ? searchResult.matches.message || [] : []}
isHovered={isHovered}
isWrapped={wrap}
scale={scale}
>
{logEntry.fields.message}
</LogTextStreamItemMessageField>
</LogTextStreamLogEntryItemDiv>
);
}
}
);
interface IconProps {
isHovered: boolean;
}
const EmptyIcon = styled.div`
width: 24px;
`;
const LogTextStreamIconDiv = styled<IconProps, 'div'>('div')`
flex-grow: 0;
background-color: ${props =>
props.isHovered
? props.theme.darkMode
? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight))
: darken(0.05, props.theme.eui.euiColorHighlight)
: 'transparent'};
text-align: center;
user-select: none;
`;
const LogTextStreamLogEntryItemDiv = styled.div`
font-family: ${props => props.theme.eui.euiCodeFontFamily};
font-size: ${props => props.theme.eui.euiFontSize};

View file

@ -42,6 +42,8 @@ interface ScrollableLogTextStreamViewProps {
}
) => any;
loadNewerItems: () => void;
setFlyoutItem: (id: string) => void;
showFlyout: () => void;
}
interface ScrollableLogTextStreamViewState {
@ -141,7 +143,13 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
key={getStreamItemId(item)}
>
{measureRef => (
<LogTextStreamItemView ref={measureRef} item={item} scale={scale} wrap={wrap} />
<LogTextStreamItemView
openFlyoutWithItem={this.handleOpenFlyout}
ref={measureRef}
item={item}
scale={scale}
wrap={wrap}
/>
)}
</MeasurableItemView>
))}
@ -160,6 +168,11 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
}
}
private handleOpenFlyout = (id: string) => {
this.props.setFlyoutItem(id);
this.props.showFlyout();
};
private handleReload = () => {
const { jumpToTarget, target } = this.props;

View file

@ -0,0 +1,23 @@
/*
* 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';
export const flyoutItemQuery = gql`
query FlyoutItemQuery($sourceId: ID!, $itemId: ID!) {
source(id: $sourceId) {
id
logItem(id: $itemId) {
id
index
fields {
field
value
}
}
}
}
`;

View file

@ -0,0 +1,60 @@
/*
* 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 { Query } from 'react-apollo';
import { FlyoutItemQuery, InfraLogItem } from '../../graphql/types';
import { FlyoutVisibility } from '../../store/local/log_flyout';
import { WithOptions } from '../with_options';
import { flyoutItemQuery } from './flyout_item.gql_query';
import { WithFlyoutOptions } from './with_log_flyout_options';
interface WithFlyoutArgs {
flyoutItem: InfraLogItem | null;
setFlyoutItem: (id: string) => void;
showFlyout: () => void;
hideFlyout: () => void;
error?: string | undefined;
loading: boolean;
}
interface WithFlyoutProps {
children: (args: WithFlyoutArgs) => React.ReactNode;
}
export const WithLogFlyout = ({ children }: WithFlyoutProps) => {
return (
<WithOptions>
{({ sourceId }) => (
<WithFlyoutOptions>
{({ showFlyout, hideFlyout, setFlyoutItem, flyoutId, flyoutVisibility }) =>
flyoutVisibility === FlyoutVisibility.visible ? (
<Query<FlyoutItemQuery.Query, FlyoutItemQuery.Variables>
query={flyoutItemQuery}
fetchPolicy="no-cache"
variables={{
itemId: (flyoutId != null && flyoutId) || '',
sourceId,
}}
>
{({ data, error, loading }) => {
return children({
showFlyout,
hideFlyout,
setFlyoutItem,
flyoutItem: (data && data.source && data.source.logItem) || null,
error: error && error.message,
loading,
});
}}
</Query>
) : null
}
</WithFlyoutOptions>
)}
</WithOptions>
);
};

View file

@ -0,0 +1,105 @@
/*
* 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 { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { isString } from 'lodash';
import { flyoutOptionsActions, flyoutOptionsSelectors, State } from '../../store';
import { FlyoutVisibility } from '../../store/local/log_flyout';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
const selectOptionsUrlState = createSelector(
flyoutOptionsSelectors.selectFlyoutId,
flyoutOptionsSelectors.selectFlyoutVisibility,
(flyoutId, flyoutVisibility) => ({
flyoutVisibility,
flyoutId,
})
);
export const withFlyoutOptions = connect(
(state: State) => ({
flyoutVisibility: flyoutOptionsSelectors.selectFlyoutVisibility(state),
flyoutId: flyoutOptionsSelectors.selectFlyoutId(state),
urlState: selectOptionsUrlState(state),
}),
bindPlainActionCreators({
setFlyoutItem: flyoutOptionsActions.setFlyoutItem,
showFlyout: flyoutOptionsActions.showFlyout,
hideFlyout: flyoutOptionsActions.hideFlyout,
})
);
export const WithFlyoutOptions = asChildFunctionRenderer(withFlyoutOptions);
/**
* Url State
*/
interface FlyoutOptionsUrlState {
flyoutId?: ReturnType<typeof flyoutOptionsSelectors.selectFlyoutId>;
flyoutVisibility?: ReturnType<typeof flyoutOptionsSelectors.selectFlyoutVisibility>;
}
export const WithFlyoutOptionsUrlState = () => (
<WithFlyoutOptions>
{({ setFlyoutItem, showFlyout, hideFlyout, urlState }) => (
<UrlStateContainer
urlState={urlState}
urlStateKey="flyoutOptions"
mapToUrlState={mapToUrlState}
onChange={newUrlState => {
if (newUrlState && newUrlState.flyoutId) {
setFlyoutItem(newUrlState.flyoutId);
}
if (newUrlState && newUrlState.flyoutVisibility === FlyoutVisibility.visible) {
showFlyout();
}
if (newUrlState && newUrlState.flyoutVisibility === FlyoutVisibility.hidden) {
hideFlyout();
}
}}
onInitialize={initialUrlState => {
if (initialUrlState && initialUrlState.flyoutId) {
setFlyoutItem(initialUrlState.flyoutId);
}
if (initialUrlState && initialUrlState.flyoutVisibility === FlyoutVisibility.visible) {
showFlyout();
}
if (initialUrlState && initialUrlState.flyoutVisibility === FlyoutVisibility.hidden) {
hideFlyout();
}
}}
/>
)}
</WithFlyoutOptions>
);
const mapToUrlState = (value: any): FlyoutOptionsUrlState | undefined =>
value
? {
flyoutId: mapToFlyoutIdState(value.flyoutId),
flyoutVisibility: mapToFlyoutVisibilityState(value.flyoutVisibility),
}
: undefined;
const mapToFlyoutIdState = (subject: any) => {
return subject && isString(subject) ? subject : undefined;
};
const mapToFlyoutVisibilityState = (subject: any) => {
if (subject) {
if (subject === 'visible') {
return FlyoutVisibility.visible;
}
if (subject === 'hidden') {
return FlyoutVisibility.hidden;
}
}
};

View file

@ -40,11 +40,11 @@ function findOrCreateGroupWithNodes(
}
}
}
const lastPath = last(path);
const existingGroup = groups.find(g => g.id === id);
if (isWaffleMapGroupWithNodes(existingGroup)) {
return existingGroup;
}
const lastPath = last(path);
return {
id,
name:
@ -65,11 +65,11 @@ function findOrCreateGroupWithGroups(
path: InfraNodePath[]
): InfraWaffleMapGroupOfGroups {
const id = path.length === 0 ? '__all__' : createId(path);
const lastPath = last(path);
const existingGroup = groups.find(g => g.id === id);
if (isWaffleMapGroupWithGroups(existingGroup)) {
return existingGroup;
}
const lastPath = last(path);
return {
id,
name:

View file

@ -299,6 +299,29 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "logItem",
"description": "",
"args": [
{
"name": "id",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"defaultValue": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "InfraLogItem", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "map",
"description": "A hierarchy of hosts, pods, containers, services or arbitrary groups",
@ -1310,6 +1333,96 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraLogItem",
"description": "",
"fields": [
{
"name": "id",
"description": "The ID of the document",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "ID", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "index",
"description": "The index where the document was found",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fields",
"description": "An array of flattened fields and values",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "InfraLogItemField", "ofType": null }
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "InfraLogItemField",
"description": "",
"fields": [
{
"name": "field",
"description": "The flattened field name",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "value",
"description": "The value for the Field as a string",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "InfraTimerangeInput",

View file

@ -34,6 +34,8 @@ export interface InfraSource {
logEntriesBetween: InfraLogEntryInterval;
/** A consecutive span of summary buckets within an interval */
logSummaryBetween: InfraLogSummaryInterval;
logItem: InfraLogItem;
/** A hierarchy of hosts, pods, containers, services or arbitrary groups */
map?: InfraResponse | null;
@ -177,6 +179,22 @@ export interface InfraLogSummaryBucket {
entriesCount: number;
}
export interface InfraLogItem {
/** The ID of the document */
id: string;
/** The index where the document was found */
index: string;
/** An array of flattened fields and values */
fields: InfraLogItemField[];
}
export interface InfraLogItemField {
/** The flattened field name */
field: string;
/** The value for the Field as a string */
value: string;
}
export interface InfraResponse {
nodes: InfraNode[];
}
@ -395,6 +413,9 @@ export interface LogSummaryBetweenInfraSourceArgs {
/** The query to filter the log entries by */
filterQuery?: string | null;
}
export interface LogItemInfraSourceArgs {
id: string;
}
export interface MapInfraSourceArgs {
timerange: InfraTimerangeInput;
@ -522,6 +543,45 @@ export type InfraLogMessageSegment = InfraLogMessageFieldSegment | InfraLogMessa
// Documents
// ====================================================
export namespace FlyoutItemQuery {
export type Variables = {
sourceId: string;
itemId: string;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'InfraSource';
id: string;
logItem: LogItem;
};
export type LogItem = {
__typename?: 'InfraLogItem';
id: string;
index: string;
fields: Fields[];
};
export type Fields = {
__typename?: 'InfraLogItemField';
field: string;
value: string;
};
}
export namespace MetadataQuery {
export type Variables = {
sourceId: string;

View file

@ -12,10 +12,14 @@ import { LogsToolbar } from './toolbar';
import { EmptyPage } from '../../components/empty_page';
import { Header } from '../../components/header';
import { LogFlyout } from '../../components/logging/log_flyout';
import { ColumnarPage } from '../../components/page';
import { InfraHeaderFeedbackLink } from '../../components/header_feedback_link';
import { WithLogFilterUrlState } from '../../containers/logs/with_log_filter';
import { WithLogFilter, WithLogFilterUrlState } from '../../containers/logs/with_log_filter';
import { WithLogFlyout } from '../../containers/logs/with_log_flyout';
import { WithFlyoutOptions } from '../../containers/logs/with_log_flyout_options';
import { WithFlyoutOptionsUrlState } from '../../containers/logs/with_log_flyout_options';
import { WithLogMinimapUrlState } from '../../containers/logs/with_log_minimap';
import { WithLogPositionUrlState } from '../../containers/logs/with_log_position';
import { WithLogTextviewUrlState } from '../../containers/logs/with_log_textview';
@ -61,8 +65,32 @@ export const LogsPage = injectI18n(
<WithLogPositionUrlState />
<WithLogMinimapUrlState />
<WithLogTextviewUrlState />
<WithFlyoutOptionsUrlState />
<LogsToolbar />
<LogsPageContent />
<WithLogFilter indexPattern={derivedIndexPattern}>
{({ applyFilterQueryFromKueryExpression }) => (
<React.Fragment>
<WithFlyoutOptions>
{({ showFlyout, setFlyoutItem }) => (
<LogsPageContent
showFlyout={showFlyout}
setFlyoutItem={setFlyoutItem}
/>
)}
</WithFlyoutOptions>
<WithLogFlyout>
{({ flyoutItem, hideFlyout, loading }) => (
<LogFlyout
setFilter={applyFilterQueryFromKueryExpression}
flyoutItem={flyoutItem}
hideFlyout={hideFlyout}
loading={loading}
/>
)}
</WithLogFlyout>
</React.Fragment>
)}
</WithLogFilter>
</>
) : isLoading ? (
<SourceLoadingPage />

View file

@ -17,7 +17,12 @@ import { WithLogTextview } from '../../containers/logs/with_log_textview';
import { WithStreamItems } from '../../containers/logs/with_stream_items';
import { WithSummary } from '../../containers/logs/with_summary';
export const LogsPageContent: React.SFC = () => (
interface Props {
setFlyoutItem: (id: string) => void;
showFlyout: () => void;
}
export const LogsPageContent: React.SFC<Props> = ({ showFlyout, setFlyoutItem }) => (
<PageContent>
<AutoSizer content>
{({ measureRef, content: { width = 0, height = 0 } }) => (
@ -57,6 +62,8 @@ export const LogsPageContent: React.SFC = () => (
target={targetPosition}
width={width}
wrap={wrap}
setFlyoutItem={setFlyoutItem}
showFlyout={showFlyout}
/>
)}
</WithStreamItems>

View file

@ -13,5 +13,6 @@ export {
waffleFilterActions,
waffleTimeActions,
waffleOptionsActions,
flyoutOptionsActions,
} from './local';
export { logEntriesActions, logSummaryActions } from './remote';

View file

@ -12,3 +12,4 @@ export { metricTimeActions } from './metric_time';
export { waffleFilterActions } from './waffle_filter';
export { waffleTimeActions } from './waffle_time';
export { waffleOptionsActions } from './waffle_options';
export { flyoutOptionsActions } from './log_flyout';

View file

@ -0,0 +1,11 @@
/*
* 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 actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory('x-pack/infra/local/log_flyout');
export const setFlyoutItem = actionCreator<string>('SET_FLYOUT_ITEM');
export const showFlyout = actionCreator('SHOW_FLYOUT');
export const hideFlyout = actionCreator('HIDE_FLYOUT');

View file

@ -0,0 +1,11 @@
/*
* 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 * as flyoutOptionsActions from './actions';
import * as flyoutOptionsSelectors from './selector';
export { flyoutOptionsActions, flyoutOptionsSelectors };
export * from './reducer';

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { combineReducers } from 'redux';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { hideFlyout, setFlyoutItem, showFlyout } from './actions';
export enum FlyoutVisibility {
hidden = 'hidden',
visible = 'visible',
}
export interface FlyoutOptionsState {
visibility: FlyoutVisibility;
itemId: string;
}
export const initialFlyoutOptionsState: FlyoutOptionsState = {
visibility: FlyoutVisibility.hidden,
itemId: '',
};
const currentFlyoutReducer = reducerWithInitialState(initialFlyoutOptionsState.itemId).case(
setFlyoutItem,
(current, target) => target
);
const currentFlyoutVisibilityReducer = reducerWithInitialState(initialFlyoutOptionsState.visibility)
.case(hideFlyout, () => FlyoutVisibility.hidden)
.case(showFlyout, () => FlyoutVisibility.visible);
export const flyoutOptionsReducer = combineReducers<FlyoutOptionsState>({
itemId: currentFlyoutReducer,
visibility: currentFlyoutVisibilityReducer,
});

View file

@ -0,0 +1,10 @@
/*
* 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 { FlyoutOptionsState } from './reducer';
export const selectFlyoutId = (state: FlyoutOptionsState) => state.itemId;
export const selectFlyoutVisibility = (state: FlyoutOptionsState) => state.visibility;

View file

@ -7,6 +7,7 @@
import { combineReducers } from 'redux';
import { initialLogFilterState, logFilterReducer, LogFilterState } from './log_filter';
import { flyoutOptionsReducer, FlyoutOptionsState, initialFlyoutOptionsState } from './log_flyout';
import { initialLogMinimapState, logMinimapReducer, LogMinimapState } from './log_minimap';
import { initialLogPositionState, logPositionReducer, LogPositionState } from './log_position';
import { initialLogTextviewState, logTextviewReducer, LogTextviewState } from './log_textview';
@ -28,6 +29,7 @@ export interface LocalState {
waffleFilter: WaffleFilterState;
waffleTime: WaffleTimeState;
waffleMetrics: WaffleOptionsState;
logFlyout: FlyoutOptionsState;
}
export const initialLocalState: LocalState = {
@ -39,6 +41,7 @@ export const initialLocalState: LocalState = {
waffleFilter: initialWaffleFilterState,
waffleTime: initialWaffleTimeState,
waffleMetrics: initialWaffleOptionsState,
logFlyout: initialFlyoutOptionsState,
};
export const localReducer = combineReducers<LocalState>({
@ -50,4 +53,5 @@ export const localReducer = combineReducers<LocalState>({
waffleFilter: waffleFilterReducer,
waffleTime: waffleTimeReducer,
waffleMetrics: waffleOptionsReducer,
logFlyout: flyoutOptionsReducer,
});

View file

@ -6,6 +6,7 @@
import { globalizeSelectors } from '../../utils/typed_redux';
import { logFilterSelectors as innerLogFilterSelectors } from './log_filter';
import { flyoutOptionsSelectors as innerFlyoutOptionsSelectors } from './log_flyout';
import { logMinimapSelectors as innerLogMinimapSelectors } from './log_minimap';
import { logPositionSelectors as innerLogPositionSelectors } from './log_position';
import { logTextviewSelectors as innerLogTextviewSelectors } from './log_textview';
@ -54,3 +55,8 @@ export const waffleOptionsSelectors = globalizeSelectors(
(state: LocalState) => state.waffleMetrics,
innerWaffleOptionsSelectors
);
export const flyoutOptionsSelectors = globalizeSelectors(
(state: LocalState) => state.logFlyout,
innerFlyoutOptionsSelectors
);

View file

@ -9,6 +9,7 @@ import { createSelector } from 'reselect';
import { getLogEntryAtTime } from '../utils/log_entry';
import { globalizeSelectors } from '../utils/typed_redux';
import {
flyoutOptionsSelectors as localFlyoutOptionsSelectors,
logFilterSelectors as localLogFilterSelectors,
logMinimapSelectors as localLogMinimapSelectors,
logPositionSelectors as localLogPositionSelectors,
@ -38,6 +39,7 @@ export const metricTimeSelectors = globalizeSelectors(selectLocal, localMetricTi
export const waffleFilterSelectors = globalizeSelectors(selectLocal, localWaffleFilterSelectors);
export const waffleTimeSelectors = globalizeSelectors(selectLocal, localWaffleTimeSelectors);
export const waffleOptionsSelectors = globalizeSelectors(selectLocal, localWaffleOptionsSelectors);
export const flyoutOptionsSelectors = globalizeSelectors(selectLocal, localFlyoutOptionsSelectors);
/**
* remote selectors

View file

@ -31,6 +31,11 @@ export type InfraSourceLogSummaryBetweenResolver = ChildResolverOf<
QuerySourceResolver
>;
export type InfraSourceLogItem = ChildResolverOf<
InfraResolverOf<InfraSourceResolvers.LogItemResolver>,
QuerySourceResolver
>;
export const createLogEntriesResolvers = (libs: {
logEntries: InfraLogEntriesDomain;
}): {
@ -38,6 +43,7 @@ export const createLogEntriesResolvers = (libs: {
logEntriesAround: InfraSourceLogEntriesAroundResolver;
logEntriesBetween: InfraSourceLogEntriesBetweenResolver;
logSummaryBetween: InfraSourceLogSummaryBetweenResolver;
logItem: InfraSourceLogItem;
};
InfraLogMessageSegment: {
__resolveType(
@ -115,6 +121,9 @@ export const createLogEntriesResolvers = (libs: {
buckets,
};
},
async logItem(source, args, { req }) {
return await libs.logEntries.getLogItem(req, args.id, source.configuration);
},
},
InfraLogMessageSegment: {
__resolveType: (messageSegment: InfraLogMessageSegment) => {

View file

@ -78,6 +78,22 @@ export const logEntriesSchema = gql`
buckets: [InfraLogSummaryBucket!]!
}
type InfraLogItemField {
"The flattened field name"
field: String!
"The value for the Field as a string"
value: String!
}
type InfraLogItem {
"The ID of the document"
id: ID!
"The index where the document was found"
index: String!
"An array of flattened fields and values"
fields: [InfraLogItemField!]!
}
extend type InfraSource {
"A consecutive span of log entries surrounding a point in time"
logEntriesAround(
@ -114,5 +130,6 @@ export const logEntriesSchema = gql`
"The query to filter the log entries by"
filterQuery: String
): InfraLogSummaryInterval!
logItem(id: ID!): InfraLogItem!
}
`;

View file

@ -62,6 +62,8 @@ export interface InfraSource {
logEntriesBetween: InfraLogEntryInterval;
/** A consecutive span of summary buckets within an interval */
logSummaryBetween: InfraLogSummaryInterval;
logItem: InfraLogItem;
/** A hierarchy of hosts, pods, containers, services or arbitrary groups */
map?: InfraResponse | null;
@ -205,6 +207,22 @@ export interface InfraLogSummaryBucket {
entriesCount: number;
}
export interface InfraLogItem {
/** The ID of the document */
id: string;
/** The index where the document was found */
index: string;
/** An array of flattened fields and values */
fields: InfraLogItemField[];
}
export interface InfraLogItemField {
/** The flattened field name */
field: string;
/** The value for the Field as a string */
value: string;
}
export interface InfraResponse {
nodes: InfraNode[];
}
@ -423,6 +441,9 @@ export interface LogSummaryBetweenInfraSourceArgs {
/** The query to filter the log entries by */
filterQuery?: string | null;
}
export interface LogItemInfraSourceArgs {
id: string;
}
export interface MapInfraSourceArgs {
timerange: InfraTimerangeInput;
@ -596,6 +617,8 @@ export namespace InfraSourceResolvers {
logEntriesBetween?: LogEntriesBetweenResolver<InfraLogEntryInterval, TypeParent, Context>;
/** A consecutive span of summary buckets within an interval */
logSummaryBetween?: LogSummaryBetweenResolver<InfraLogSummaryInterval, TypeParent, Context>;
logItem?: LogItemResolver<InfraLogItem, TypeParent, Context>;
/** A hierarchy of hosts, pods, containers, services or arbitrary groups */
map?: MapResolver<InfraResponse | null, TypeParent, Context>;
@ -688,6 +711,15 @@ export namespace InfraSourceResolvers {
filterQuery?: string | null;
}
export type LogItemResolver<
R = InfraLogItem,
Parent = InfraSource,
Context = InfraContext
> = Resolver<R, Parent, Context, LogItemArgs>;
export interface LogItemArgs {
id: string;
}
export type MapResolver<
R = InfraResponse | null,
Parent = InfraSource,
@ -1144,6 +1176,53 @@ export namespace InfraLogSummaryBucketResolvers {
> = Resolver<R, Parent, Context>;
}
export namespace InfraLogItemResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogItem> {
/** The ID of the document */
id?: IdResolver<string, TypeParent, Context>;
/** The index where the document was found */
index?: IndexResolver<string, TypeParent, Context>;
/** An array of flattened fields and values */
fields?: FieldsResolver<InfraLogItemField[], TypeParent, Context>;
}
export type IdResolver<R = string, Parent = InfraLogItem, Context = InfraContext> = Resolver<
R,
Parent,
Context
>;
export type IndexResolver<R = string, Parent = InfraLogItem, Context = InfraContext> = Resolver<
R,
Parent,
Context
>;
export type FieldsResolver<
R = InfraLogItemField[],
Parent = InfraLogItem,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
export namespace InfraLogItemFieldResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraLogItemField> {
/** The flattened field name */
field?: FieldResolver<string, TypeParent, Context>;
/** The value for the Field as a string */
value?: ValueResolver<string, TypeParent, Context>;
}
export type FieldResolver<
R = string,
Parent = InfraLogItemField,
Context = InfraContext
> = Resolver<R, Parent, Context>;
export type ValueResolver<
R = string,
Parent = InfraLogItemField,
Context = InfraContext
> = Resolver<R, Parent, Context>;
}
export namespace InfraResponseResolvers {
export interface Resolvers<Context = InfraContext, TypeParent = InfraResponse> {
nodes?: NodesResolver<InfraNode[], TypeParent, Context>;

View file

@ -5,10 +5,12 @@
*/
import { timeMilliseconds } from 'd3-time';
import first from 'lodash/fp/first';
import get from 'lodash/fp/get';
import has from 'lodash/fp/has';
import zip from 'lodash/fp/zip';
import { JsonObject } from 'x-pack/plugins/infra/common/typed_json';
import { compareTimeKeys, isTimeKey, TimeKey } from '../../../../common/time';
import {
LogEntriesAdapter,
@ -28,6 +30,12 @@ const DAY_MILLIS = 24 * 60 * 60 * 1000;
const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000, Infinity].map(days => days * DAY_MILLIS);
const TIMESTAMP_FORMAT = 'epoch_millis';
interface LogItemHit {
_index: string;
_id: string;
_source: JsonObject;
}
export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
constructor(private readonly framework: InfraBackendFrameworkAdapter) {}
@ -152,6 +160,35 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter {
: [];
}
public async getLogItem(
request: InfraFrameworkRequest,
id: string,
sourceConfiguration: InfraSourceConfiguration
) {
const search = (searchOptions: object) =>
this.framework.callWithRequest<LogItemHit, {}>(request, 'search', searchOptions);
const params = {
index: sourceConfiguration.logAlias,
terminate_after: 1,
body: {
size: 1,
query: {
ids: {
values: [id],
},
},
},
};
const response = await search(params);
const document = first(response.hits.hits);
if (!document) {
throw new Error('Document not found');
}
return document;
}
private async getLogEntryDocumentsBetween(
request: InfraFrameworkRequest,
sourceConfiguration: InfraSourceConfiguration,

View file

@ -0,0 +1,67 @@
/*
* 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 { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields';
describe('convertDocumentSourceToLogItemFields', () => {
test('should convert document', () => {
const doc = {
agent: {
hostname: 'demo-stack-client-01',
id: '7adef8b6-2ab7-45cd-a0d5-b3baad735f1b',
type: 'filebeat',
ephemeral_id: 'a0c8164b-3564-4e32-b0bf-f4db5a7ae566',
version: '7.0.0',
},
tags: ['prod', 'web'],
metadata: [{ key: 'env', value: 'prod' }, { key: 'stack', value: 'web' }],
host: {
hostname: 'packer-virtualbox-iso-1546820004',
name: 'demo-stack-client-01',
},
};
const fields = convertDocumentSourceToLogItemFields(doc);
expect(fields).toEqual([
{
field: 'agent.hostname',
value: 'demo-stack-client-01',
},
{
field: 'agent.id',
value: '7adef8b6-2ab7-45cd-a0d5-b3baad735f1b',
},
{
field: 'agent.type',
value: 'filebeat',
},
{
field: 'agent.ephemeral_id',
value: 'a0c8164b-3564-4e32-b0bf-f4db5a7ae566',
},
{
field: 'agent.version',
value: '7.0.0',
},
{
field: 'tags',
value: '["prod","web"]',
},
{
field: 'metadata',
value: '[{"key":"env","value":"prod"},{"key":"stack","value":"web"}]',
},
{
field: 'host.hostname',
value: 'packer-virtualbox-iso-1546820004',
},
{
field: 'host.name',
value: 'demo-stack-client-01',
},
]);
});
});

View file

@ -0,0 +1,36 @@
/*
* 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 { isArray, isPlainObject } from 'lodash';
import { JsonObject } from 'x-pack/plugins/infra/common/typed_json';
import { InfraLogItemField } from '../../../graphql/types';
const isJsonObject = (subject: any): subject is JsonObject => {
return isPlainObject(subject);
};
const serializeValue = (value: any): string => {
if (isArray(value) || isPlainObject(value)) {
return JSON.stringify(value);
}
return `${value}`;
};
export const convertDocumentSourceToLogItemFields = (
source: JsonObject,
path: string[] = [],
fields: InfraLogItemField[] = []
): InfraLogItemField[] => {
return Object.keys(source).reduce((acc, key) => {
const value = source[key];
const nextPath = [...path, key];
if (isJsonObject(value)) {
return convertDocumentSourceToLogItemFields(value, nextPath, acc);
}
const field = { field: nextPath.join('.'), value: serializeValue(value) };
return [...acc, field];
}, fields);
};

View file

@ -4,8 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { sortBy } from 'lodash';
import { TimeKey } from '../../../../common/time';
import { JsonObject } from '../../../../common/typed_json';
import { InfraLogItem } from '../../../graphql/types';
import {
InfraLogEntry,
InfraLogMessageSegment,
@ -14,6 +16,7 @@ import {
import { InfraDateRangeAggregationBucket, InfraFrameworkRequest } from '../../adapters/framework';
import { InfraSourceConfiguration, InfraSources } from '../../sources';
import { builtinRules } from './builtin_rules';
import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields';
import { compileFormattingRules } from './message';
export class InfraLogEntriesDomain {
@ -121,6 +124,32 @@ export class InfraLogEntriesDomain {
const buckets = dateRangeBuckets.map(convertDateRangeBucketToSummaryBucket);
return buckets;
}
public async getLogItem(
request: InfraFrameworkRequest,
id: string,
sourceConfiguration: InfraSourceConfiguration
): Promise<InfraLogItem> {
const document = await this.adapter.getLogItem(request, id, sourceConfiguration);
const defaultFields = [
{ field: '_index', value: document._index },
{ field: '_id', value: document._id },
];
return {
id: document._id,
index: document._index,
fields: sortBy(
[...defaultFields, ...convertDocumentSourceToLogItemFields(document._source)],
'field'
),
};
}
}
interface LogItemHit {
_index: string;
_id: string;
_source: JsonObject;
}
export interface LogEntriesAdapter {
@ -153,6 +182,12 @@ export interface LogEntriesAdapter {
bucketSize: number,
filterQuery?: LogEntryQuery
): Promise<InfraDateRangeAggregationBucket[]>;
getLogItem(
request: InfraFrameworkRequest,
id: string,
source: InfraSourceConfiguration
): Promise<LogItemHit>;
}
export type LogEntryQuery = JsonObject;

View file

@ -14,5 +14,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./metrics'));
loadTestFile(require.resolve('./sources'));
loadTestFile(require.resolve('./waffle'));
loadTestFile(require.resolve('./log_item'));
});
}

View file

@ -0,0 +1,175 @@
/*
* 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 expect from 'expect.js';
import { flyoutItemQuery } from '../../../../plugins/infra/public/containers/logs/flyout_item.gql_query';
import { FlyoutItemQuery } from '../../../../plugins/infra/public/graphql/types';
import { KbnTestProvider } from './types';
const logItemTests: KbnTestProvider = ({ getService }) => {
const esArchiver = getService('esArchiver');
const client = getService('infraOpsGraphQLClient');
describe('Log Item GraphQL Endpoint', () => {
before(() => esArchiver.load('infra/metrics_and_logs'));
after(() => esArchiver.unload('infra/metrics_and_logs'));
it('should basically work', () => {
return client
.query<FlyoutItemQuery.Query>({
query: flyoutItemQuery,
variables: {
sourceId: 'default',
itemId: 'yT2Mg2YBh-opCxJv8Vqj',
},
})
.then(resp => {
expect(resp.data.source).to.have.property('logItem');
const { logItem } = resp.data.source;
if (!logItem) {
throw new Error('Log item should not be falsey');
}
expect(logItem).to.have.property('id', 'yT2Mg2YBh-opCxJv8Vqj');
expect(logItem).to.have.property('index', 'filebeat-7.0.0-alpha1-2018.10.17');
expect(logItem).to.have.property('fields');
expect(logItem.fields).to.eql([
{
field: '@timestamp',
value: '2018-10-17T19:42:22.000Z',
__typename: 'InfraLogItemField',
},
{
field: '_id',
value: 'yT2Mg2YBh-opCxJv8Vqj',
__typename: 'InfraLogItemField',
},
{
field: '_index',
value: 'filebeat-7.0.0-alpha1-2018.10.17',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.body_sent.bytes',
value: '1336',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.http_version',
value: '1.1',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.method',
value: 'GET',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.referrer',
value: '-',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.remote_ip',
value: '10.128.0.11',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.response_code',
value: '200',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.url',
value: '/a-fresh-start-will-put-you-on-your-way',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.user_agent.device',
value: 'Other',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.user_agent.name',
value: 'Other',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.user_agent.os',
value: 'Other',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.user_agent.os_name',
value: 'Other',
__typename: 'InfraLogItemField',
},
{
field: 'apache2.access.user_name',
value: '-',
__typename: 'InfraLogItemField',
},
{
field: 'beat.hostname',
value: 'demo-stack-apache-01',
__typename: 'InfraLogItemField',
},
{
field: 'beat.name',
value: 'demo-stack-apache-01',
__typename: 'InfraLogItemField',
},
{
field: 'beat.version',
value: '7.0.0-alpha1',
__typename: 'InfraLogItemField',
},
{
field: 'fileset.module',
value: 'apache2',
__typename: 'InfraLogItemField',
},
{
field: 'fileset.name',
value: 'access',
__typename: 'InfraLogItemField',
},
{
field: 'host.name',
value: 'demo-stack-apache-01',
__typename: 'InfraLogItemField',
},
{
field: 'input.type',
value: 'log',
__typename: 'InfraLogItemField',
},
{
field: 'offset',
value: '5497614',
__typename: 'InfraLogItemField',
},
{
field: 'prospector.type',
value: 'log',
__typename: 'InfraLogItemField',
},
{
field: 'read_timestamp',
value: '2018-10-17T19:42:23.160Z',
__typename: 'InfraLogItemField',
},
{
field: 'source',
value: '/var/log/apache2/access.log',
__typename: 'InfraLogItemField',
},
]);
});
});
});
};
// tslint:disable-next-line no-default-export
export default logItemTests;

View file

@ -1708,4 +1708,4 @@
},
"aliases": {}
}
}
}

View file

@ -10745,4 +10745,4 @@
},
"aliases": {}
}
}
}