From bb7a67af2405bb404ae4f29b7c704cbb52baa0f8 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 31 Jan 2019 09:39:28 -0700 Subject: [PATCH] [Log UI] Flyout for Log Events (#28885) (#29729) * 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 * 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 --- .../public/components/logging/log_flyout.tsx | 114 ++++++++++++ .../log_text_stream/item_date_field.tsx | 1 + .../logging/log_text_stream/item_field.tsx | 2 +- .../log_text_stream/item_message_field.tsx | 1 - .../logging/log_text_stream/item_view.tsx | 4 +- .../log_text_stream/log_entry_item_view.tsx | 138 +++++++++----- .../scrollable_log_text_stream_view.tsx | 15 +- .../containers/logs/flyout_item.gql_query.ts | 23 +++ .../containers/logs/with_log_flyout.tsx | 60 ++++++ .../logs/with_log_flyout_options.tsx | 105 +++++++++++ .../containers/waffle/nodes_to_wafflemap.ts | 4 +- .../infra/public/graphql/introspection.json | 113 +++++++++++ x-pack/plugins/infra/public/graphql/types.ts | 60 ++++++ .../plugins/infra/public/pages/logs/logs.tsx | 32 +++- .../infra/public/pages/logs/page_content.tsx | 9 +- x-pack/plugins/infra/public/store/actions.ts | 1 + .../infra/public/store/local/actions.ts | 1 + .../public/store/local/log_flyout/actions.ts | 11 ++ .../public/store/local/log_flyout/index.ts | 11 ++ .../public/store/local/log_flyout/reducer.ts | 39 ++++ .../public/store/local/log_flyout/selector.ts | 10 + .../infra/public/store/local/reducer.ts | 4 + .../infra/public/store/local/selectors.ts | 6 + .../plugins/infra/public/store/selectors.ts | 2 + .../server/graphql/log_entries/resolvers.ts | 9 + .../server/graphql/log_entries/schema.gql.ts | 17 ++ x-pack/plugins/infra/server/graphql/types.ts | 79 ++++++++ .../log_entries/kibana_log_entries_adapter.ts | 37 ++++ ...document_source_to_log_item_fields.test.ts | 67 +++++++ ...vert_document_source_to_log_item_fields.ts | 36 ++++ .../log_entries_domain/log_entries_domain.ts | 35 ++++ .../test/api_integration/apis/infra/index.js | 1 + .../api_integration/apis/infra/log_item.ts | 175 ++++++++++++++++++ .../logs_without_epoch_millis/mappings.json | 2 +- .../infra/metrics_and_logs/mappings.json | 2 +- 35 files changed, 1170 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/logging/log_flyout.tsx create mode 100644 x-pack/plugins/infra/public/containers/logs/flyout_item.gql_query.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/with_log_flyout.tsx create mode 100644 x-pack/plugins/infra/public/containers/logs/with_log_flyout_options.tsx create mode 100644 x-pack/plugins/infra/public/store/local/log_flyout/actions.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_flyout/index.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_flyout/reducer.ts create mode 100644 x-pack/plugins/infra/public/store/local/log_flyout/selector.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts create mode 100644 x-pack/test/api_integration/apis/infra/log_item.ts diff --git a/x-pack/plugins/infra/public/components/logging/log_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_flyout.tsx new file mode 100644 index 0000000000000..7c277dd78ba78 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_flyout.tsx @@ -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) => ( + + + + + {item.value} + + ), + }, + ]; + return ( + hideFlyout()} size="m"> + + +

+ +

+
+
+ + {loading || flyoutItem === null ? ( + + + + ) : ( + + )} + +
+ ); + } +); + +export const InfraFlyoutLoadingPanel = styled.div` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_date_field.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_date_field.tsx index f75056e23aef0..8550033e471c6 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_date_field.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_date_field.tsx @@ -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 : '')}; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_field.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_field.tsx index c9d360fc116a7..4b6dcdb635ad9 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_field.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_field.tsx @@ -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; `; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_message_field.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_message_field.tsx index 77ef813261818..39f75d5b7ab64 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_message_field.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_message_field.tsx @@ -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}; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_view.tsx index c4a75771fa5a0..055b92250b0f8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/item_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/item_view.tsx @@ -14,10 +14,11 @@ interface StreamItemProps { item: StreamItem; scale: TextScale; wrap: boolean; + openFlyoutWithItem: (id: string) => void; } export const LogTextStreamItemView = React.forwardRef( - ({ item, scale, wrap }, ref) => { + ({ item, scale, wrap, openFlyoutWithItem }, ref) => { switch (item.kind) { case 'logEntry': return ( @@ -27,6 +28,7 @@ export const LogTextStreamItemView = React.forwardRef( searchResult={item.searchResult} scale={scale} wrap={wrap} + openFlyoutWithItem={openFlyoutWithItem} /> ); } diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_item_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_item_view.tsx index 2e910cad56785..fb8642c3bd680 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_item_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_item_view.tsx @@ -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, - }; +export const LogTextStreamLogEntryItemView = injectI18n( + class extends React.PureComponent< + LogTextStreamLogEntryItemViewProps, + LogTextStreamLogEntryItemViewState + > { + public readonly state = { + isHovered: false, + }; - public handleMouseEnter: React.MouseEventHandler = () => { - this.setState({ - isHovered: true, - }); - }; + public handleMouseEnter: React.MouseEventHandler = () => { + this.setState({ + isHovered: true, + }); + }; - public handleMouseLeave: React.MouseEventHandler = () => { - this.setState({ - isHovered: false, - }); - }; + public handleMouseLeave: React.MouseEventHandler = () => { + this.setState({ + isHovered: false, + }); + }; - public render() { - const { boundingBoxRef, logEntry, scale, searchResult, wrap } = this.props; - const { isHovered } = this.state; + public handleClick: React.MouseEventHandler = () => { + this.props.openFlyoutWithItem(this.props.logEntry.gid); + }; - return ( - - - {formatTime(logEntry.fields.time)} - - - {logEntry.fields.message} - - - ); + + {formatTime(logEntry.fields.time)} + + + {isHovered ? ( + + + + ) : ( + + )} + + + {logEntry.fields.message} + + + ); + } } +); + +interface IconProps { + isHovered: boolean; } +const EmptyIcon = styled.div` + width: 24px; +`; + +const LogTextStreamIconDiv = styled('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}; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index eb74ffcc29394..47db7a6716de8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -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 => ( - + )} ))} @@ -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; diff --git a/x-pack/plugins/infra/public/containers/logs/flyout_item.gql_query.ts b/x-pack/plugins/infra/public/containers/logs/flyout_item.gql_query.ts new file mode 100644 index 0000000000000..56b63a54b1126 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/flyout_item.gql_query.ts @@ -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 + } + } + } + } +`; diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_flyout.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_flyout.tsx new file mode 100644 index 0000000000000..71c0a330fcb97 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_flyout.tsx @@ -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 ( + + {({ sourceId }) => ( + + {({ showFlyout, hideFlyout, setFlyoutItem, flyoutId, flyoutVisibility }) => + flyoutVisibility === FlyoutVisibility.visible ? ( + + 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, + }); + }} + + ) : null + } + + )} + + ); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/with_log_flyout_options.tsx b/x-pack/plugins/infra/public/containers/logs/with_log_flyout_options.tsx new file mode 100644 index 0000000000000..13e011cda876b --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/with_log_flyout_options.tsx @@ -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; + flyoutVisibility?: ReturnType; +} + +export const WithFlyoutOptionsUrlState = () => ( + + {({ setFlyoutItem, showFlyout, hideFlyout, urlState }) => ( + { + 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(); + } + }} + /> + )} + +); + +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; + } + } +}; diff --git a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts b/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts index 59a015d6d5ca3..ad623aa756770 100644 --- a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts +++ b/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts @@ -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: diff --git a/x-pack/plugins/infra/public/graphql/introspection.json b/x-pack/plugins/infra/public/graphql/introspection.json index 5fad4b5cdc2b6..db057cc7f286e 100644 --- a/x-pack/plugins/infra/public/graphql/introspection.json +++ b/x-pack/plugins/infra/public/graphql/introspection.json @@ -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", diff --git a/x-pack/plugins/infra/public/graphql/types.ts b/x-pack/plugins/infra/public/graphql/types.ts index ce622a3283304..7143afe44257e 100644 --- a/x-pack/plugins/infra/public/graphql/types.ts +++ b/x-pack/plugins/infra/public/graphql/types.ts @@ -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; diff --git a/x-pack/plugins/infra/public/pages/logs/logs.tsx b/x-pack/plugins/infra/public/pages/logs/logs.tsx index d62e83ef9491a..2223e0413d1ee 100644 --- a/x-pack/plugins/infra/public/pages/logs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/logs/logs.tsx @@ -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 { LogsBetaBadgeHeaderSection } from '../../components/beta_badge_header_section'; -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( + - + + {({ applyFilterQueryFromKueryExpression }) => ( + + + {({ showFlyout, setFlyoutItem }) => ( + + )} + + + {({ flyoutItem, hideFlyout, loading }) => ( + + )} + + + )} + ) : isLoading ? ( diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index e2efd2f1a333a..2ca93a310323a 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -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 = ({ showFlyout, setFlyoutItem }) => ( {({ 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} /> )} diff --git a/x-pack/plugins/infra/public/store/actions.ts b/x-pack/plugins/infra/public/store/actions.ts index ee9a2858f1c34..68a20f4dba98f 100644 --- a/x-pack/plugins/infra/public/store/actions.ts +++ b/x-pack/plugins/infra/public/store/actions.ts @@ -13,5 +13,6 @@ export { waffleFilterActions, waffleTimeActions, waffleOptionsActions, + flyoutOptionsActions, } from './local'; export { logEntriesActions, logSummaryActions } from './remote'; diff --git a/x-pack/plugins/infra/public/store/local/actions.ts b/x-pack/plugins/infra/public/store/local/actions.ts index 8b9e0c9f5b58a..ef988c9b956a0 100644 --- a/x-pack/plugins/infra/public/store/local/actions.ts +++ b/x-pack/plugins/infra/public/store/local/actions.ts @@ -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'; diff --git a/x-pack/plugins/infra/public/store/local/log_flyout/actions.ts b/x-pack/plugins/infra/public/store/local/log_flyout/actions.ts new file mode 100644 index 0000000000000..f0d75030dce1f --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_flyout/actions.ts @@ -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('SET_FLYOUT_ITEM'); +export const showFlyout = actionCreator('SHOW_FLYOUT'); +export const hideFlyout = actionCreator('HIDE_FLYOUT'); diff --git a/x-pack/plugins/infra/public/store/local/log_flyout/index.ts b/x-pack/plugins/infra/public/store/local/log_flyout/index.ts new file mode 100644 index 0000000000000..8dda5c3890ce6 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_flyout/index.ts @@ -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'; diff --git a/x-pack/plugins/infra/public/store/local/log_flyout/reducer.ts b/x-pack/plugins/infra/public/store/local/log_flyout/reducer.ts new file mode 100644 index 0000000000000..695dc5e7d49b7 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_flyout/reducer.ts @@ -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({ + itemId: currentFlyoutReducer, + visibility: currentFlyoutVisibilityReducer, +}); diff --git a/x-pack/plugins/infra/public/store/local/log_flyout/selector.ts b/x-pack/plugins/infra/public/store/local/log_flyout/selector.ts new file mode 100644 index 0000000000000..5024290b7bdc7 --- /dev/null +++ b/x-pack/plugins/infra/public/store/local/log_flyout/selector.ts @@ -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; diff --git a/x-pack/plugins/infra/public/store/local/reducer.ts b/x-pack/plugins/infra/public/store/local/reducer.ts index 59e890b748d5e..d7cff2f004f3f 100644 --- a/x-pack/plugins/infra/public/store/local/reducer.ts +++ b/x-pack/plugins/infra/public/store/local/reducer.ts @@ -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({ @@ -50,4 +53,5 @@ export const localReducer = combineReducers({ waffleFilter: waffleFilterReducer, waffleTime: waffleTimeReducer, waffleMetrics: waffleOptionsReducer, + logFlyout: flyoutOptionsReducer, }); diff --git a/x-pack/plugins/infra/public/store/local/selectors.ts b/x-pack/plugins/infra/public/store/local/selectors.ts index 85188e144ade1..46e6fbafb686e 100644 --- a/x-pack/plugins/infra/public/store/local/selectors.ts +++ b/x-pack/plugins/infra/public/store/local/selectors.ts @@ -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 +); diff --git a/x-pack/plugins/infra/public/store/selectors.ts b/x-pack/plugins/infra/public/store/selectors.ts index 9bb76af5bf0e0..27b77b9e3b70b 100644 --- a/x-pack/plugins/infra/public/store/selectors.ts +++ b/x-pack/plugins/infra/public/store/selectors.ts @@ -8,6 +8,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, @@ -37,6 +38,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 diff --git a/x-pack/plugins/infra/server/graphql/log_entries/resolvers.ts b/x-pack/plugins/infra/server/graphql/log_entries/resolvers.ts index 8a0dd691f9978..0c7049c1895a4 100644 --- a/x-pack/plugins/infra/server/graphql/log_entries/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/log_entries/resolvers.ts @@ -31,6 +31,11 @@ export type InfraSourceLogSummaryBetweenResolver = ChildResolverOf< QuerySourceResolver >; +export type InfraSourceLogItem = ChildResolverOf< + InfraResolverOf, + 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) => { diff --git a/x-pack/plugins/infra/server/graphql/log_entries/schema.gql.ts b/x-pack/plugins/infra/server/graphql/log_entries/schema.gql.ts index e4947767ffbb1..302b5a374b524 100644 --- a/x-pack/plugins/infra/server/graphql/log_entries/schema.gql.ts +++ b/x-pack/plugins/infra/server/graphql/log_entries/schema.gql.ts @@ -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! } `; diff --git a/x-pack/plugins/infra/server/graphql/types.ts b/x-pack/plugins/infra/server/graphql/types.ts index 10193b2998ee6..c3685a90eb981 100644 --- a/x-pack/plugins/infra/server/graphql/types.ts +++ b/x-pack/plugins/infra/server/graphql/types.ts @@ -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; /** A consecutive span of summary buckets within an interval */ logSummaryBetween?: LogSummaryBetweenResolver; + + logItem?: LogItemResolver; /** A hierarchy of hosts, pods, containers, services or arbitrary groups */ map?: MapResolver; @@ -688,6 +711,15 @@ export namespace InfraSourceResolvers { filterQuery?: string | null; } + export type LogItemResolver< + R = InfraLogItem, + Parent = InfraSource, + Context = InfraContext + > = Resolver; + export interface LogItemArgs { + id: string; + } + export type MapResolver< R = InfraResponse | null, Parent = InfraSource, @@ -1144,6 +1176,53 @@ export namespace InfraLogSummaryBucketResolvers { > = Resolver; } +export namespace InfraLogItemResolvers { + export interface Resolvers { + /** The ID of the document */ + id?: IdResolver; + /** The index where the document was found */ + index?: IndexResolver; + /** An array of flattened fields and values */ + fields?: FieldsResolver; + } + + export type IdResolver = Resolver< + R, + Parent, + Context + >; + export type IndexResolver = Resolver< + R, + Parent, + Context + >; + export type FieldsResolver< + R = InfraLogItemField[], + Parent = InfraLogItem, + Context = InfraContext + > = Resolver; +} + +export namespace InfraLogItemFieldResolvers { + export interface Resolvers { + /** The flattened field name */ + field?: FieldResolver; + /** The value for the Field as a string */ + value?: ValueResolver; + } + + export type FieldResolver< + R = string, + Parent = InfraLogItemField, + Context = InfraContext + > = Resolver; + export type ValueResolver< + R = string, + Parent = InfraLogItemField, + Context = InfraContext + > = Resolver; +} + export namespace InfraResponseResolvers { export interface Resolvers { nodes?: NodesResolver; diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index f08cfe8f5ca0e..56126c0dd8064 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -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(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, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts new file mode 100644 index 0000000000000..d46ebbb26fbb8 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts @@ -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', + }, + ]); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts new file mode 100644 index 0000000000000..923c0c22211fe --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts @@ -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); +}; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 5781ca3ad8ef8..e605efc105617 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -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 { + 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; + + getLogItem( + request: InfraFrameworkRequest, + id: string, + source: InfraSourceConfiguration + ): Promise; } export type LogEntryQuery = JsonObject; diff --git a/x-pack/test/api_integration/apis/infra/index.js b/x-pack/test/api_integration/apis/infra/index.js index 595f931a1e8fc..1471e4fe8084a 100644 --- a/x-pack/test/api_integration/apis/infra/index.js +++ b/x-pack/test/api_integration/apis/infra/index.js @@ -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')); }); } diff --git a/x-pack/test/api_integration/apis/infra/log_item.ts b/x-pack/test/api_integration/apis/infra/log_item.ts new file mode 100644 index 0000000000000..bfdde32fa57ae --- /dev/null +++ b/x-pack/test/api_integration/apis/infra/log_item.ts @@ -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({ + 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; diff --git a/x-pack/test/functional/es_archives/infra/logs_without_epoch_millis/mappings.json b/x-pack/test/functional/es_archives/infra/logs_without_epoch_millis/mappings.json index f86edda0d4c77..313c87607aab5 100644 --- a/x-pack/test/functional/es_archives/infra/logs_without_epoch_millis/mappings.json +++ b/x-pack/test/functional/es_archives/infra/logs_without_epoch_millis/mappings.json @@ -1710,4 +1710,4 @@ }, "aliases": {} } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/infra/metrics_and_logs/mappings.json b/x-pack/test/functional/es_archives/infra/metrics_and_logs/mappings.json index c460aa14b3ab1..4ca69a7d99959 100644 --- a/x-pack/test/functional/es_archives/infra/metrics_and_logs/mappings.json +++ b/x-pack/test/functional/es_archives/infra/metrics_and_logs/mappings.json @@ -10749,4 +10749,4 @@ }, "aliases": {} } -} \ No newline at end of file +}