diff --git a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index 5f35eb89774fa..31bc62f48791a 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts @@ -5,8 +5,8 @@ */ import * as rt from 'io-ts'; +import { logEntryCursorRT } from '../../log_entry'; import { jsonArrayRT } from '../../typed_json'; -import { logEntriesCursorRT } from './common'; import { logSourceColumnConfigurationRT } from '../log_sources'; export const LOG_ENTRIES_PATH = '/api/log_entries/entries'; @@ -26,17 +26,17 @@ export const logEntriesBaseRequestRT = rt.intersection([ export const logEntriesBeforeRequestRT = rt.intersection([ logEntriesBaseRequestRT, - rt.type({ before: rt.union([logEntriesCursorRT, rt.literal('last')]) }), + rt.type({ before: rt.union([logEntryCursorRT, rt.literal('last')]) }), ]); export const logEntriesAfterRequestRT = rt.intersection([ logEntriesBaseRequestRT, - rt.type({ after: rt.union([logEntriesCursorRT, rt.literal('first')]) }), + rt.type({ after: rt.union([logEntryCursorRT, rt.literal('first')]) }), ]); export const logEntriesCenteredRequestRT = rt.intersection([ logEntriesBaseRequestRT, - rt.type({ center: logEntriesCursorRT }), + rt.type({ center: logEntryCursorRT }), ]); export const logEntriesRequestRT = rt.union([ @@ -85,7 +85,7 @@ export const logEntryContextRT = rt.union([ export const logEntryRT = rt.type({ id: rt.string, - cursor: logEntriesCursorRT, + cursor: logEntryCursorRT, columns: rt.array(logColumnRT), context: logEntryContextRT, }); @@ -104,8 +104,8 @@ export const logEntriesResponseRT = rt.type({ data: rt.intersection([ rt.type({ entries: rt.array(logEntryRT), - topCursor: rt.union([logEntriesCursorRT, rt.null]), - bottomCursor: rt.union([logEntriesCursorRT, rt.null]), + topCursor: rt.union([logEntryCursorRT, rt.null]), + bottomCursor: rt.union([logEntryCursorRT, rt.null]), }), rt.partial({ hasMoreBefore: rt.boolean, diff --git a/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts b/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts index 811cf85db8883..648da43134a27 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/highlights.ts @@ -5,6 +5,7 @@ */ import * as rt from 'io-ts'; +import { logEntryCursorRT } from '../../log_entry'; import { logEntriesBaseRequestRT, logEntriesBeforeRequestRT, @@ -12,7 +13,6 @@ import { logEntriesCenteredRequestRT, logEntryRT, } from './entries'; -import { logEntriesCursorRT } from './common'; export const LOG_ENTRIES_HIGHLIGHTS_PATH = '/api/log_entries/highlights'; @@ -58,8 +58,8 @@ export const logEntriesHighlightsResponseRT = rt.type({ entries: rt.array(logEntryRT), }), rt.type({ - topCursor: logEntriesCursorRT, - bottomCursor: logEntriesCursorRT, + topCursor: logEntryCursorRT, + bottomCursor: logEntryCursorRT, entries: rt.array(logEntryRT), }), ]) diff --git a/x-pack/plugins/infra/common/http_api/log_entries/index.ts b/x-pack/plugins/infra/common/http_api/log_entries/index.ts index 490f295cbff68..9e34c1fc91199 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/index.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/index.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './common'; export * from './entries'; export * from './highlights'; -export * from './item'; export * from './summary'; export * from './summary_highlights'; diff --git a/x-pack/plugins/infra/common/http_api/log_entries/item.ts b/x-pack/plugins/infra/common/http_api/log_entries/item.ts deleted file mode 100644 index 5f9457b8228ac..0000000000000 --- a/x-pack/plugins/infra/common/http_api/log_entries/item.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 rt from 'io-ts'; -import { logEntriesCursorRT } from './common'; - -export const LOG_ENTRIES_ITEM_PATH = '/api/log_entries/item'; - -export const logEntriesItemRequestRT = rt.type({ - sourceId: rt.string, - id: rt.string, -}); - -export type LogEntriesItemRequest = rt.TypeOf; - -const logEntriesItemFieldRT = rt.type({ field: rt.string, value: rt.array(rt.string) }); -const logEntriesItemRT = rt.type({ - id: rt.string, - index: rt.string, - fields: rt.array(logEntriesItemFieldRT), - key: logEntriesCursorRT, -}); -export const logEntriesItemResponseRT = rt.type({ - data: logEntriesItemRT, -}); - -export type LogEntriesItemField = rt.TypeOf; -export type LogEntriesItem = rt.TypeOf; -export type LogEntriesItemResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/log_entries/summary_highlights.ts b/x-pack/plugins/infra/common/http_api/log_entries/summary_highlights.ts index 30222cd71bbde..7da1e7bc71c79 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/summary_highlights.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/summary_highlights.ts @@ -5,8 +5,8 @@ */ import * as rt from 'io-ts'; +import { logEntryCursorRT } from '../../log_entry'; import { logEntriesSummaryRequestRT, logEntriesSummaryBucketRT } from './summary'; -import { logEntriesCursorRT } from './common'; export const LOG_ENTRIES_SUMMARY_HIGHLIGHTS_PATH = '/api/log_entries/summary_highlights'; @@ -24,7 +24,7 @@ export type LogEntriesSummaryHighlightsRequest = rt.TypeOf< export const logEntriesSummaryHighlightsBucketRT = rt.intersection([ logEntriesSummaryBucketRT, rt.type({ - representativeKey: logEntriesCursorRT, + representativeKey: logEntryCursorRT, }), ]); diff --git a/x-pack/plugins/infra/common/log_entry/index.ts b/x-pack/plugins/infra/common/log_entry/index.ts index 66cc5108b6692..0654735499fab 100644 --- a/x-pack/plugins/infra/common/log_entry/index.ts +++ b/x-pack/plugins/infra/common/log_entry/index.ts @@ -5,3 +5,4 @@ */ export * from './log_entry'; +export * from './log_entry_cursor'; diff --git a/x-pack/plugins/infra/common/log_entry/log_entry_cursor.ts b/x-pack/plugins/infra/common/log_entry/log_entry_cursor.ts new file mode 100644 index 0000000000000..280403dd5438d --- /dev/null +++ b/x-pack/plugins/infra/common/log_entry/log_entry_cursor.ts @@ -0,0 +1,21 @@ +/* + * 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 rt from 'io-ts'; +import { decodeOrThrow } from '../runtime_types'; + +export const logEntryCursorRT = rt.type({ + time: rt.number, + tiebreaker: rt.number, +}); + +export type LogEntryCursor = rt.TypeOf; + +export const getLogEntryCursorFromHit = (hit: { sort: [number, number] }) => + decodeOrThrow(logEntryCursorRT)({ + time: hit.sort[0], + tiebreaker: hit.sort[1], + }); diff --git a/x-pack/plugins/infra/common/runtime_types.ts b/x-pack/plugins/infra/common/runtime_types.ts index a8d5cd8693a3d..a26121a5dd225 100644 --- a/x-pack/plugins/infra/common/runtime_types.ts +++ b/x-pack/plugins/infra/common/runtime_types.ts @@ -7,16 +7,41 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { Errors, Type } from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; -import { RouteValidationFunction } from 'kibana/server'; +import { Context, Errors, IntersectionType, Type, UnionType, ValidationError } from 'io-ts'; +import type { RouteValidationFunction } from 'kibana/server'; type ErrorFactory = (message: string) => Error; +const getErrorPath = ([first, ...rest]: Context): string[] => { + if (typeof first === 'undefined') { + return []; + } else if (first.type instanceof IntersectionType) { + const [, ...next] = rest; + return getErrorPath(next); + } else if (first.type instanceof UnionType) { + const [, ...next] = rest; + return [first.key, ...getErrorPath(next)]; + } + + return [first.key, ...getErrorPath(rest)]; +}; + +const getErrorType = ({ context }: ValidationError) => + context[context.length - 1]?.type?.name ?? 'unknown'; + +const formatError = (error: ValidationError) => + error.message ?? + `in ${getErrorPath(error.context).join('/')}: ${JSON.stringify( + error.value + )} does not match expected type ${getErrorType(error)}`; + +const formatErrors = (errors: ValidationError[]) => + `Failed to validate: \n${errors.map((error) => ` ${formatError(error)}`).join('\n')}`; + export const createPlainError = (message: string) => new Error(message); export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => { - throw createError(failure(errors).join('\n')); + throw createError(formatErrors(errors)); }; export const decodeOrThrow = ( @@ -33,7 +58,7 @@ export const createValidationFunction = pipe( runtimeType.decode(inputValue), fold>( - (errors: Errors) => badRequest(failure(errors).join('\n')), + (errors: Errors) => badRequest(formatErrors(errors)), (result: DecodedValue) => ok(result) ) ); diff --git a/x-pack/plugins/infra/common/search_strategies/common/errors.ts b/x-pack/plugins/infra/common/search_strategies/common/errors.ts new file mode 100644 index 0000000000000..4f7954c09c48b --- /dev/null +++ b/x-pack/plugins/infra/common/search_strategies/common/errors.ts @@ -0,0 +1,26 @@ +/* + * 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 rt from 'io-ts'; + +const genericErrorRT = rt.type({ + type: rt.literal('generic'), + message: rt.string, +}); + +const shardFailureErrorRT = rt.type({ + type: rt.literal('shardFailure'), + shardInfo: rt.type({ + shard: rt.number, + index: rt.string, + node: rt.string, + }), + message: rt.string, +}); + +export const searchStrategyErrorRT = rt.union([genericErrorRT, shardFailureErrorRT]); + +export type SearchStrategyError = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entry.ts b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entry.ts new file mode 100644 index 0000000000000..af6bd203f980e --- /dev/null +++ b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entry.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { logEntryCursorRT } from '../../log_entry'; +import { jsonArrayRT } from '../../typed_json'; +import { searchStrategyErrorRT } from '../common/errors'; + +export const LOG_ENTRY_SEARCH_STRATEGY = 'infra-log-entry'; + +export const logEntrySearchRequestParamsRT = rt.type({ + sourceId: rt.string, + logEntryId: rt.string, +}); + +export type LogEntrySearchRequestParams = rt.TypeOf; + +const logEntryFieldRT = rt.type({ + field: rt.string, + value: jsonArrayRT, +}); + +export type LogEntryField = rt.TypeOf; + +export const logEntryRT = rt.type({ + id: rt.string, + index: rt.string, + fields: rt.array(logEntryFieldRT), + key: logEntryCursorRT, +}); + +export type LogEntry = rt.TypeOf; + +export const logEntrySearchResponsePayloadRT = rt.intersection([ + rt.type({ + data: rt.union([logEntryRT, rt.null]), + }), + rt.partial({ + errors: rt.array(searchStrategyErrorRT), + }), +]); + +export type LogEntrySearchResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index c4e6bbe094642..3ca1ed7d4726f 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useCallback, useEffect } from 'react'; import { noop } from 'lodash'; import { euiStyled } from '../../../../observability/public'; -import { LogEntriesCursor } from '../../../common/http_api'; +import { LogEntryCursor } from '../../../common/log_entry'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { LogSourceConfigurationProperties, useLogSource } from '../../containers/logs/log_source'; @@ -28,7 +28,7 @@ export interface LogStreamProps { startTimestamp: number; endTimestamp: number; query?: string; - center?: LogEntriesCursor; + center?: LogEntryCursor; highlight?: string; height?: string | number; columns?: LogColumnDefinition[]; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx index 77154474077c8..f578292d6d6fc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx @@ -28,7 +28,7 @@ describe('LogEntryActionsMenu component', () => { const elementWrapper = mount( { const elementWrapper = mount( { const elementWrapper = mount( { const elementWrapper = mount( { const elementWrapper = mount( { const elementWrapper = mount( { const elementWrapper = mount( { const elementWrapper = mount( = ({ logItem }) => { + logEntry: LogEntry; +}> = ({ logEntry }) => { const { hide, isVisible, show } = useVisibilityState(false); - const apmLinkDescriptor = useMemo(() => getAPMLink(logItem), [logItem]); - const uptimeLinkDescriptor = useMemo(() => getUptimeLink(logItem), [logItem]); + const apmLinkDescriptor = useMemo(() => getAPMLink(logEntry), [logEntry]); + const uptimeLinkDescriptor = useMemo(() => getUptimeLink(logEntry), [logEntry]); const uptimeLinkProps = useLinkProps({ app: 'uptime', @@ -90,8 +90,8 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ ); }; -const getUptimeLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { - const searchExpressions = logItem.fields +const getUptimeLink = (logEntry: LogEntry): LinkDescriptor | undefined => { + const searchExpressions = logEntry.fields .filter(({ field, value }) => value != null && UPTIME_FIELDS.includes(field)) .reduce((acc, fieldItem) => { const { field, value } = fieldItem; @@ -110,31 +110,32 @@ const getUptimeLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { }; }; -const getAPMLink = (logItem: LogEntriesItem): LinkDescriptor | undefined => { - const traceIdEntry = logItem.fields.find( - ({ field, value }) => value[0] != null && field === 'trace.id' - ); +const getAPMLink = (logEntry: LogEntry): LinkDescriptor | undefined => { + const traceId = logEntry.fields.find( + ({ field, value }) => typeof value[0] === 'string' && field === 'trace.id' + )?.value?.[0]; - if (!traceIdEntry) { + if (typeof traceId !== 'string') { return undefined; } - const timestampField = logItem.fields.find(({ field }) => field === '@timestamp'); + const timestampField = logEntry.fields.find(({ field }) => field === '@timestamp'); const timestamp = timestampField ? timestampField.value[0] : null; - const { rangeFrom, rangeTo } = timestamp - ? (() => { - const from = new Date(timestamp); - const to = new Date(timestamp); + const { rangeFrom, rangeTo } = + typeof timestamp === 'number' + ? (() => { + const from = new Date(timestamp); + const to = new Date(timestamp); - from.setMinutes(from.getMinutes() - 10); - to.setMinutes(to.getMinutes() + 10); + from.setMinutes(from.getMinutes() - 10); + to.setMinutes(to.getMinutes() + 10); - return { rangeFrom: from.toISOString(), rangeTo: to.toISOString() }; - })() - : { rangeFrom: 'now-1y', rangeTo: 'now' }; + return { rangeFrom: from.toISOString(), rangeTo: to.toISOString() }; + })() + : { rangeFrom: 'now-1y', rangeTo: 'now' }; return { app: 'apm', - hash: getTraceUrl({ traceId: traceIdEntry.value[0], rangeFrom, rangeTo }), + hash: getTraceUrl({ traceId, rangeFrom, rangeTo }), }; }; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx index b07d8c9dce23c..bc0f6dc97017a 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx @@ -5,13 +5,16 @@ */ import { - EuiBasicTable, + EuiBasicTableColumn, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, + EuiInMemoryTable, + EuiSpacer, + EuiTextColor, EuiTitle, EuiToolTip, } from '@elastic/eui'; @@ -19,28 +22,49 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; import React, { useCallback, useMemo } from 'react'; - import { euiStyled } from '../../../../../observability/public'; +import { + LogEntry, + LogEntryField, +} from '../../../../common/search_strategies/log_entries/log_entry'; import { TimeKey } from '../../../../common/time'; import { InfraLoadingPanel } from '../../loading'; +import { FieldValue } from '../log_text_stream/field_value'; import { LogEntryActionsMenu } from './log_entry_actions_menu'; -import { LogEntriesItem, LogEntriesItemField } from '../../../../common/http_api'; export interface LogEntryFlyoutProps { - flyoutItem: LogEntriesItem | null; + flyoutError: string | null; + flyoutItem: LogEntry | null; setFlyoutVisibility: (visible: boolean) => void; setFilter: (filter: string, flyoutItemId: string, timeKey?: TimeKey) => void; loading: boolean; } +const emptyHighlightTerms: string[] = []; + +const initialSortingOptions = { + sort: { + field: 'field', + direction: 'asc' as const, + }, +}; + +const searchOptions = { + box: { + incremental: true, + schema: true, + }, +}; + export const LogEntryFlyout = ({ + flyoutError, flyoutItem, loading, setFlyoutVisibility, setFilter, }: LogEntryFlyoutProps) => { const createFilterHandler = useCallback( - (field: LogEntriesItemField) => () => { + (field: LogEntryField) => () => { if (!flyoutItem) { return; } @@ -63,7 +87,7 @@ export const LogEntryFlyout = ({ const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]); - const columns = useMemo( + const columns = useMemo>>( () => [ { field: 'field', @@ -77,8 +101,7 @@ export const LogEntryFlyout = ({ name: i18n.translate('xpack.infra.logFlyout.valueColumnLabel', { defaultMessage: 'Value', }), - sortable: true, - render: (_name: string, item: LogEntriesItemField) => ( + render: (_name: string, item: LogEntryField) => ( - {formatValue(item.value)} + ), }, @@ -110,19 +137,36 @@ export const LogEntryFlyout = ({

{flyoutItem.id} : '', + }} />

+ {flyoutItem ? ( + <> + + + {flyoutItem.index}, + }} + /> + + + ) : null} - {flyoutItem !== null ? : null} + {flyoutItem !== null ? : null} - {loading || flyoutItem === null ? ( + {loading ? ( + ) : flyoutItem ? ( + + columns={columns} + items={flyoutItem.fields} + search={searchOptions} + sorting={initialSortingOptions} + /> ) : ( - + {flyoutError} )} @@ -147,7 +198,3 @@ export const InfraFlyoutLoadingPanel = euiStyled.div` bottom: 0; left: 0; `; - -function formatValue(value: string[]) { - return value.length > 1 ? value.join(', ') : value[0]; -} diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entries_item.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entries_item.ts deleted file mode 100644 index d459fba6cf957..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entries_item.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 type { HttpHandler } from 'src/core/public'; - -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -import { - LOG_ENTRIES_ITEM_PATH, - LogEntriesItemRequest, - logEntriesItemRequestRT, - logEntriesItemResponseRT, -} from '../../../../../common/http_api'; - -export const fetchLogEntriesItem = async ( - requestArgs: LogEntriesItemRequest, - fetch: HttpHandler -) => { - const response = await fetch(LOG_ENTRIES_ITEM_PATH, { - method: 'POST', - body: JSON.stringify(logEntriesItemRequestRT.encode(requestArgs)), - }); - - return decodeOrThrow(logEntriesItemResponseRT)(response); -}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts new file mode 100644 index 0000000000000..764de1d34a3bf --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts @@ -0,0 +1,31 @@ +/* + * 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 { ISearchStart } from '../../../../../../../../src/plugins/data/public'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { + LogEntry, + LogEntrySearchRequestParams, + logEntrySearchRequestParamsRT, + logEntrySearchResponsePayloadRT, + LOG_ENTRY_SEARCH_STRATEGY, +} from '../../../../../common/search_strategies/log_entries/log_entry'; + +export { LogEntry }; + +export const fetchLogEntry = async ( + requestArgs: LogEntrySearchRequestParams, + search: ISearchStart +) => { + const response = await search + .search( + { params: logEntrySearchRequestParamsRT.encode(requestArgs) }, + { strategy: LOG_ENTRY_SEARCH_STRATEGY } + ) + .toPromise(); + + return decodeOrThrow(logEntrySearchResponsePayloadRT)(response.rawResponse); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx index 9ed2f5ad175c7..121f0e6b651dc 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx @@ -7,12 +7,10 @@ import createContainer from 'constate'; import { isString } from 'lodash'; import React, { useContext, useEffect, useMemo, useState } from 'react'; - -import { LogEntriesItem } from '../../../common/http_api'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { UrlStateContainer } from '../../utils/url_state'; import { useTrackedPromise } from '../../utils/use_tracked_promise'; -import { fetchLogEntriesItem } from './log_entries/api/fetch_log_entries_item'; +import { fetchLogEntry } from './log_entries/api/fetch_log_entry'; import { useLogSourceContext } from './log_source'; export enum FlyoutVisibility { @@ -31,7 +29,6 @@ export const useLogFlyout = () => { const { sourceId } = useLogSourceContext(); const [flyoutVisible, setFlyoutVisibility] = useState(false); const [flyoutId, setFlyoutId] = useState(null); - const [flyoutItem, setFlyoutItem] = useState(null); const [surroundingLogsId, setSurroundingLogsId] = useState(null); const [loadFlyoutItemRequest, loadFlyoutItem] = useTrackedPromise( @@ -39,15 +36,9 @@ export const useLogFlyout = () => { cancelPreviousOn: 'creation', createPromise: async () => { if (!flyoutId) { - return; - } - return await fetchLogEntriesItem({ sourceId, id: flyoutId }, services.http.fetch); - }, - onResolve: (response) => { - if (response) { - const { data } = response; - setFlyoutItem(data || null); + throw new Error('Failed to load log entry: Id not specified.'); } + return await fetchLogEntry({ sourceId, logEntryId: flyoutId }, services.data.search); }, }, [sourceId, flyoutId] @@ -71,7 +62,10 @@ export const useLogFlyout = () => { surroundingLogsId, setSurroundingLogsId, isLoading, - flyoutItem, + flyoutItem: + loadFlyoutItemRequest.state === 'resolved' ? loadFlyoutItemRequest.value.data : null, + flyoutError: + loadFlyoutItemRequest.state === 'rejected' ? `${loadFlyoutItemRequest.value}` : null, }; }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index b0b09c76f4d85..ff30e993aa3a9 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -10,7 +10,8 @@ import usePrevious from 'react-use/lib/usePrevious'; import { esKuery } from '../../../../../../../src/plugins/data/public'; import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { LogEntry, LogEntriesCursor } from '../../../../common/http_api'; +import { LogEntry } from '../../../../common/http_api'; +import { LogEntryCursor } from '../../../../common/log_entry'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { LogSourceConfigurationProperties } from '../log_source'; @@ -19,14 +20,14 @@ interface LogStreamProps { startTimestamp: number; endTimestamp: number; query?: string; - center?: LogEntriesCursor; + center?: LogEntryCursor; columns?: LogSourceConfigurationProperties['logColumns']; } interface LogStreamState { entries: LogEntry[]; - topCursor: LogEntriesCursor | null; - bottomCursor: LogEntriesCursor | null; + topCursor: LogEntryCursor | null; + bottomCursor: LogEntryCursor | null; hasMoreBefore: boolean; hasMoreAfter: boolean; } diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index b33eaf7e77bc3..bb0c9196fb0cc 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -144,9 +144,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { filteredDatasets: selectedDatasets, }); - const { flyoutVisible, setFlyoutVisibility, flyoutItem, isLoading: isFlyoutLoading } = useContext( - LogFlyout.Context - ); + const { + flyoutVisible, + setFlyoutVisibility, + flyoutError, + flyoutItem, + isLoading: isFlyoutLoading, + } = useContext(LogFlyout.Context); const handleQueryTimeRangeChange = useCallback( ({ start: startTime, end: endTime }: { start: string; end: string }) => { @@ -304,6 +308,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { {flyoutVisible ? ( { surroundingLogsId, setSurroundingLogsId, flyoutItem, + flyoutError, isLoading, } = useContext(LogFlyoutState.Context); const { logSummaryHighlights } = useContext(LogHighlightsState.Context); @@ -80,6 +81,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { setFilter={setFilter} setFlyoutVisibility={setFlyoutVisibility} flyoutItem={flyoutItem} + flyoutError={flyoutError} loading={isLoading} /> ) : null} diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 2bf5687da7e08..6c0d4e9d302ee 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -34,7 +34,6 @@ import { initLogEntriesHighlightsRoute, initLogEntriesSummaryRoute, initLogEntriesSummaryHighlightsRoute, - initLogEntriesItemRoute, } from './routes/log_entries'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; @@ -74,7 +73,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initLogEntriesHighlightsRoute(libs); initLogEntriesSummaryRoute(libs); initLogEntriesSummaryHighlightsRoute(libs); - initLogEntriesItemRoute(libs); initMetricExplorerRoute(libs); initMetricsAPIRoute(libs); initMetadataRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index ad82939ec7f9d..93a7bc9a0830b 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -8,6 +8,10 @@ import { GenericParams, SearchResponse } from 'elasticsearch'; import { Lifecycle } from '@hapi/hapi'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { RouteConfig, RouteMethod } from '../../../../../../../src/core/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../../../../src/plugins/data/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginSetup } from '../../../../../../plugins/apm/server'; @@ -17,7 +21,8 @@ import { PluginSetupContract as AlertingPluginContract } from '../../../../../al import { MlPluginSetup } from '../../../../../ml/server'; import { JsonArray, JsonValue } from '../../../../common/typed_json'; -export interface InfraServerPluginDeps { +export interface InfraServerPluginSetupDeps { + data: DataPluginSetup; home: HomeServerPluginSetup; spaces: SpacesPluginSetup; usageCollection: UsageCollectionSetup; @@ -28,6 +33,10 @@ export interface InfraServerPluginDeps { ml?: MlPluginSetup; } +export interface InfraServerPluginStartDeps { + data: DataPluginStart; +} + export interface CallWithRequestParams extends GenericParams { max_concurrent_shard_requests?: number; name?: string; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 2d84e36f3a3ac..7f686b4d7717c 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -10,7 +10,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { InfraRouteConfig, InfraTSVBResponse, - InfraServerPluginDeps, + InfraServerPluginSetupDeps, CallWithRequestParams, InfraDatabaseSearchResponse, InfraDatabaseMultiResponse, @@ -33,9 +33,9 @@ import { IndexPatternsFetcher, UI_SETTINGS } from '../../../../../../../src/plug export class KibanaFramework { public router: IRouter; - public plugins: InfraServerPluginDeps; + public plugins: InfraServerPluginSetupDeps; - constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { + constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginSetupDeps) { this.router = core.http.createRouter(); this.plugins = plugins; } 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 6ffa1ad4b0b82..4637f3ab41782 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 @@ -9,12 +9,11 @@ import { fold, map } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as runtimeTypes from 'io-ts'; -import { compact, first } from 'lodash'; +import { compact } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; import { JsonArray } from '../../../../common/typed_json'; import { LogEntriesAdapter, - LogItemHit, LogEntriesParams, LogEntryDocument, LogEntryQuery, @@ -199,41 +198,6 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { fold(constant([]), identity) ); } - - public async getLogItem( - requestContext: RequestHandlerContext, - id: string, - sourceConfiguration: InfraSourceConfiguration - ) { - const search = (searchOptions: object) => - this.framework.callWithRequest(requestContext, 'search', searchOptions); - - const params = { - index: sourceConfiguration.logAlias, - terminate_after: 1, - body: { - size: 1, - sort: [ - { [sourceConfiguration.fields.timestamp]: 'desc' }, - { [sourceConfiguration.fields.tiebreaker]: 'desc' }, - ], - query: { - ids: { - values: [id], - }, - }, - fields: ['*'], - _source: false, - }, - }; - - const response = await search(params); - const document = first(response.hits.hits); - if (!document) { - throw new Error('Document not found'); - } - return document; - } } function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]): LogEntryDocument[] { 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 deleted file mode 100644 index 7b79a1bf0386a..0000000000000 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 { convertESFieldsToLogItemFields } from './convert_document_source_to_log_item_fields'; - -describe('convertESFieldsToLogItemFields', () => { - test('Converts the fields collection to LogItemFields', () => { - const esFields = { - 'agent.hostname': ['demo-stack-client-01'], - 'agent.id': ['7adef8b6-2ab7-45cd-a0d5-b3baad735f1b'], - 'agent.type': ['filebeat'], - 'agent.ephemeral_id': ['a0c8164b-3564-4e32-b0bf-f4db5a7ae566'], - 'agent.version': ['7.0.0'], - tags: ['prod', 'web'], - metadata: [ - { key: 'env', value: 'prod' }, - { key: 'stack', value: 'web' }, - ], - 'host.hostname': ['packer-virtualbox-iso-1546820004'], - 'host.name': ['demo-stack-client-01'], - }; - - const fields = convertESFieldsToLogItemFields(esFields); - 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 deleted file mode 100644 index a1d855bfdaa48..0000000000000 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 stringify from 'json-stable-stringify'; -import { LogEntriesItemField } from '../../../../common/http_api'; -import { JsonArray } from '../../../../common/typed_json'; - -const serializeValue = (value: JsonArray): string[] => { - return value.map((v) => { - if (typeof v === 'object' && v != null) { - return stringify(v); - } else { - return `${v}`; - } - }); -}; - -export const convertESFieldsToLogItemFields = (fields: { - [field: string]: JsonArray; -}): LogEntriesItemField[] => { - return Object.keys(fields).map((field) => ({ field, value: serializeValue(fields[field]) })); -}; 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 e10eb1d7e8aad..52cf6f46716b3 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,16 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; - import { RequestHandlerContext } from 'src/core/server'; -import { JsonArray, JsonObject } from '../../../../common/typed_json'; +import { JsonObject } from '../../../../common/typed_json'; import { LogEntriesSummaryBucket, LogEntriesSummaryHighlightsBucket, LogEntry, - LogEntriesItem, - LogEntriesCursor, LogColumn, LogEntriesRequest, } from '../../../../common/http_api'; @@ -23,7 +19,6 @@ import { SavedSourceConfigurationFieldColumnRuntimeType, } from '../../sources'; import { getBuiltinRules } from './builtin_rules'; -import { convertESFieldsToLogItemFields } from './convert_document_source_to_log_item_fields'; import { CompiledLogMessageFormattingRule, Fields, @@ -38,20 +33,21 @@ import { CompositeDatasetKey, createLogEntryDatasetsQuery, } from './queries/log_entry_datasets'; +import { LogEntryCursor } from '../../../../common/log_entry'; export interface LogEntriesParams { startTimestamp: number; endTimestamp: number; size?: number; query?: JsonObject; - cursor?: { before: LogEntriesCursor | 'last' } | { after: LogEntriesCursor | 'first' }; + cursor?: { before: LogEntryCursor | 'last' } | { after: LogEntryCursor | 'first' }; highlightTerm?: string; } export interface LogEntriesAroundParams { startTimestamp: number; endTimestamp: number; size?: number; - center: LogEntriesCursor; + center: LogEntryCursor; query?: JsonObject; highlightTerm?: string; } @@ -259,31 +255,6 @@ export class InfraLogEntriesDomain { return summaries; } - public async getLogItem( - requestContext: RequestHandlerContext, - id: string, - sourceConfiguration: InfraSourceConfiguration - ): Promise { - const document = await this.adapter.getLogItem(requestContext, id, sourceConfiguration); - const defaultFields = [ - { field: '_index', value: [document._index] }, - { field: '_id', value: [document._id] }, - ]; - - return { - id: document._id, - index: document._index, - key: { - time: document.sort[0], - tiebreaker: document.sort[1], - }, - fields: sortBy( - [...defaultFields, ...convertESFieldsToLogItemFields(document.fields)], - 'field' - ), - }; - } - public async getLogEntryDatasets( requestContext: RequestHandlerContext, timestampField: string, @@ -324,13 +295,6 @@ export class InfraLogEntriesDomain { } } -export interface LogItemHit { - _index: string; - _id: string; - fields: { [field: string]: [value: JsonArray] }; - sort: [number, number]; -} - export interface LogEntriesAdapter { getLogEntries( requestContext: RequestHandlerContext, @@ -347,12 +311,6 @@ export interface LogEntriesAdapter { bucketSize: number, filterQuery?: LogEntryQuery ): Promise; - - getLogItem( - requestContext: RequestHandlerContext, - id: string, - source: InfraSourceConfiguration - ): Promise; } export type LogEntryQuery = JsonObject; @@ -361,14 +319,14 @@ export interface LogEntryDocument { id: string; fields: Fields; highlights: Highlights; - cursor: LogEntriesCursor; + cursor: LogEntryCursor; } export interface LogSummaryBucket { entriesCount: number; start: number; end: number; - topEntryKeys: LogEntriesCursor[]; + topEntryKeys: LogEntryCursor[]; } const logSummaryBucketHasEntries = (bucket: LogSummaryBucket) => diff --git a/x-pack/plugins/infra/server/lib/sources/mocks.ts b/x-pack/plugins/infra/server/lib/sources/mocks.ts new file mode 100644 index 0000000000000..c48340e87a631 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/mocks.ts @@ -0,0 +1,19 @@ +/* + * 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 type { InfraSources } from './sources'; + +type IInfraSources = Pick; + +export const createInfraSourcesMock = (): jest.Mocked => ({ + getSourceConfiguration: jest.fn(), + createSourceConfiguration: jest.fn(), + deleteSourceConfiguration: jest.fn(), + updateSourceConfiguration: jest.fn(), + getAllSourceConfigurations: jest.fn(), + getInternalSourceConfiguration: jest.fn(), + defineInternalSourceConfiguration: jest.fn(), +}); diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 65acc2b2756bd..d144b079b41e8 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -28,6 +28,9 @@ interface Libs { config: InfraConfig; } +// extract public interface +export type IInfraSources = Pick; + export class InfraSources { private internalSourceConfigurations: Map = new Map(); private readonly libs: Libs; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index ef09dbfcb2674..693e98521ada2 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -4,32 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { Server } from '@hapi/hapi'; -import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { Observable } from 'rxjs'; +import { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; +import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; +import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; +import { LOGS_FEATURE, METRICS_FEATURE } from './features'; import { initInfraServer } from './infra_server'; -import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; import { FrameworkFieldsAdapter } from './lib/adapters/fields/framework_fields_adapter'; +import { InfraServerPluginSetupDeps, InfraServerPluginStartDeps } from './lib/adapters/framework'; import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter'; import { InfraKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter'; import { KibanaMetricsAdapter } from './lib/adapters/metrics/kibana_metrics_adapter'; import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_status'; +import { registerAlertTypes } from './lib/alerting'; import { InfraFieldsDomain } from './lib/domains/fields_domain'; import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain'; import { InfraMetricsDomain } from './lib/domains/metrics_domain'; +import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; +import { infraSourceConfigurationSavedObjectType, InfraSources } from './lib/sources'; import { InfraSourceStatus } from './lib/source_status'; -import { InfraSources } from './lib/sources'; -import { InfraServerPluginDeps } from './lib/adapters/framework'; -import { METRICS_FEATURE, LOGS_FEATURE } from './features'; -import { UsageCollector } from './usage/usage_collector'; -import { InfraStaticSourceConfiguration } from '../common/http_api/source_api'; -import { registerAlertTypes } from './lib/alerting'; -import { infraSourceConfigurationSavedObjectType } from './lib/sources'; -import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; -import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; +import { LogEntriesService } from './services/log_entries'; import { InfraRequestHandlerContext } from './types'; +import { UsageCollector } from './usage/usage_collector'; export const config = { schema: schema.object({ @@ -87,7 +87,7 @@ export class InfraServerPlugin { this.config$ = context.config.create(); } - async setup(core: CoreSetup, plugins: InfraServerPluginDeps) { + async setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { await new Promise((resolve) => { this.config$.subscribe((configValue) => { this.config = configValue; @@ -167,6 +167,9 @@ export class InfraServerPlugin { // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection); + const logEntriesService = new LogEntriesService(); + logEntriesService.setup(core, { ...plugins, sources }); + return { defineInternalSourceConfiguration(sourceId, sourceProperties) { sources.defineInternalSourceConfiguration(sourceId, sourceProperties); diff --git a/x-pack/plugins/infra/server/routes/log_entries/index.ts b/x-pack/plugins/infra/server/routes/log_entries/index.ts index 1090d35d89b85..9e34c1fc91199 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/index.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/index.ts @@ -6,6 +6,5 @@ export * from './entries'; export * from './highlights'; -export * from './item'; export * from './summary'; export * from './summary_highlights'; diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts deleted file mode 100644 index 67ca481ff4fcb..0000000000000 --- a/x-pack/plugins/infra/server/routes/log_entries/item.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { createValidationFunction } from '../../../common/runtime_types'; - -import { InfraBackendLibs } from '../../lib/infra_types'; -import { - LOG_ENTRIES_ITEM_PATH, - logEntriesItemRequestRT, - logEntriesItemResponseRT, -} from '../../../common/http_api'; - -export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: InfraBackendLibs) => { - framework.registerRoute( - { - method: 'post', - path: LOG_ENTRIES_ITEM_PATH, - validate: { body: createValidationFunction(logEntriesItemRequestRT) }, - }, - async (requestContext, request, response) => { - try { - const payload = request.body; - const { id, sourceId } = payload; - const sourceConfiguration = ( - await sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId) - ).configuration; - - const logEntry = await logEntries.getLogItem(requestContext, id, sourceConfiguration); - - return response.ok({ - body: logEntriesItemResponseRT.encode({ - data: logEntry, - }), - }); - } catch (error) { - return response.internalError({ body: error.message }); - } - } - ); -}; diff --git a/x-pack/plugins/infra/common/http_api/log_entries/common.ts b/x-pack/plugins/infra/server/services/log_entries/index.ts similarity index 55% rename from x-pack/plugins/infra/common/http_api/log_entries/common.ts rename to x-pack/plugins/infra/server/services/log_entries/index.ts index 0b31222322007..90b97b924fa0d 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/common.ts +++ b/x-pack/plugins/infra/server/services/log_entries/index.ts @@ -4,10 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as rt from 'io-ts'; - -export const logEntriesCursorRT = rt.type({ - time: rt.number, - tiebreaker: rt.number, -}); -export type LogEntriesCursor = rt.TypeOf; +export * from './log_entries_service'; diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_service.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_service.ts new file mode 100644 index 0000000000000..edd53be9db841 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_service.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 { CoreSetup } from 'src/core/server'; +import { LOG_ENTRY_SEARCH_STRATEGY } from '../../../common/search_strategies/log_entries/log_entry'; +import { logEntrySearchStrategyProvider } from './log_entry_search_strategy'; +import { LogEntriesServiceSetupDeps, LogEntriesServiceStartDeps } from './types'; + +export class LogEntriesService { + public setup(core: CoreSetup, setupDeps: LogEntriesServiceSetupDeps) { + core.getStartServices().then(([, startDeps]) => { + setupDeps.data.search.registerSearchStrategy( + LOG_ENTRY_SEARCH_STRATEGY, + logEntrySearchStrategyProvider({ ...setupDeps, ...startDeps }) + ); + }); + } + + public start(_startDeps: LogEntriesServiceStartDeps) {} +} diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts new file mode 100644 index 0000000000000..044cea3899baf --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -0,0 +1,225 @@ +/* + * 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 { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { of, throwError } from 'rxjs'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, + uiSettingsServiceMock, +} from 'src/core/server/mocks'; +import { + IEsSearchRequest, + IEsSearchResponse, + ISearchStrategy, + SearchStrategyDependencies, +} from 'src/plugins/data/server'; +import { createInfraSourcesMock } from '../../lib/sources/mocks'; +import { + logEntrySearchRequestStateRT, + logEntrySearchStrategyProvider, +} from './log_entry_search_strategy'; + +describe('LogEntry search strategy', () => { + it('handles initial search requests', async () => { + const esSearchStrategyMock = createEsSearchStrategyMock({ + id: 'ASYNC_REQUEST_ID', + isRunning: true, + rawResponse: { + took: 0, + _shards: { total: 1, failed: 0, skipped: 0, successful: 0 }, + timed_out: false, + hits: { total: 0, max_score: 0, hits: [] }, + }, + }); + + const dataMock = createDataPluginMock(esSearchStrategyMock); + const sourcesMock = createInfraSourcesMock(); + sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const mockDependencies = createSearchStrategyDependenciesMock(); + + const logEntrySearchStrategy = logEntrySearchStrategyProvider({ + data: dataMock, + sources: sourcesMock, + }); + + const response = await logEntrySearchStrategy + .search( + { + params: { sourceId: 'SOURCE_ID', logEntryId: 'LOG_ENTRY_ID' }, + }, + {}, + mockDependencies + ) + .toPromise(); + + expect(sourcesMock.getSourceConfiguration).toHaveBeenCalled(); + expect(esSearchStrategyMock.search).toHaveBeenCalled(); + expect(response.id).toEqual(expect.any(String)); + expect(response.isRunning).toBe(true); + }); + + it('handles subsequent polling requests', async () => { + const esSearchStrategyMock = createEsSearchStrategyMock({ + id: 'ASYNC_REQUEST_ID', + isRunning: false, + rawResponse: { + took: 1, + _shards: { total: 1, failed: 0, skipped: 0, successful: 1 }, + timed_out: false, + hits: { + total: 0, + max_score: 0, + hits: [ + { + _id: 'HIT_ID', + _index: 'HIT_INDEX', + _type: '_doc', + _score: 0, + _source: null, + fields: { + '@timestamp': [1605116827143], + message: ['HIT_MESSAGE'], + }, + sort: [1605116827143 as any, 1 as any], // incorrectly typed as string upstream + }, + ], + }, + }, + }); + const dataMock = createDataPluginMock(esSearchStrategyMock); + const sourcesMock = createInfraSourcesMock(); + sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const mockDependencies = createSearchStrategyDependenciesMock(); + + const logEntrySearchStrategy = logEntrySearchStrategyProvider({ + data: dataMock, + sources: sourcesMock, + }); + const requestId = logEntrySearchRequestStateRT.encode({ + esRequestId: 'ASYNC_REQUEST_ID', + }); + + const response = await logEntrySearchStrategy + .search( + { + id: requestId, + params: { sourceId: 'SOURCE_ID', logEntryId: 'LOG_ENTRY_ID' }, + }, + {}, + mockDependencies + ) + .toPromise(); + + expect(sourcesMock.getSourceConfiguration).not.toHaveBeenCalled(); + expect(esSearchStrategyMock.search).toHaveBeenCalled(); + expect(response.id).toEqual(requestId); + expect(response.isRunning).toBe(false); + expect(response.rawResponse.data).toEqual({ + id: 'HIT_ID', + index: 'HIT_INDEX', + key: { + time: 1605116827143, + tiebreaker: 1, + }, + fields: [ + { field: '@timestamp', value: [1605116827143] }, + { field: 'message', value: ['HIT_MESSAGE'] }, + ], + }); + }); + + it('forwards errors from the underlying search strategy', async () => { + const esSearchStrategyMock = createEsSearchStrategyMock({ + id: 'ASYNC_REQUEST_ID', + isRunning: false, + rawResponse: { + took: 1, + _shards: { total: 1, failed: 0, skipped: 0, successful: 1 }, + timed_out: false, + hits: { total: 0, max_score: 0, hits: [] }, + }, + }); + const dataMock = createDataPluginMock(esSearchStrategyMock); + const sourcesMock = createInfraSourcesMock(); + sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const mockDependencies = createSearchStrategyDependenciesMock(); + + const logEntrySearchStrategy = logEntrySearchStrategyProvider({ + data: dataMock, + sources: sourcesMock, + }); + + const response = logEntrySearchStrategy.search( + { + id: logEntrySearchRequestStateRT.encode({ esRequestId: 'UNKNOWN_ID' }), + params: { sourceId: 'SOURCE_ID', logEntryId: 'LOG_ENTRY_ID' }, + }, + {}, + mockDependencies + ); + + await expect(response.toPromise()).rejects.toThrowError(ResponseError); + }); +}); + +const createSourceConfigurationMock = () => ({ + id: 'SOURCE_ID', + origin: 'stored' as const, + configuration: { + name: 'SOURCE_NAME', + description: 'SOURCE_DESCRIPTION', + logAlias: 'log-indices-*', + metricAlias: 'metric-indices-*', + inventoryDefaultView: 'DEFAULT_VIEW', + metricsExplorerDefaultView: 'DEFAULT_VIEW', + logColumns: [], + fields: { + pod: 'POD_FIELD', + host: 'HOST_FIELD', + container: 'CONTAINER_FIELD', + message: ['MESSAGE_FIELD'], + timestamp: 'TIMESTAMP_FIELD', + tiebreaker: 'TIEBREAKER_FIELD', + }, + }, +}); + +const createEsSearchStrategyMock = (esSearchResponse: IEsSearchResponse) => ({ + search: jest.fn((esSearchRequest: IEsSearchRequest) => { + if (typeof esSearchRequest.id === 'string') { + if (esSearchRequest.id === esSearchResponse.id) { + return of(esSearchResponse); + } else { + return throwError( + new ResponseError({ + body: {}, + headers: {}, + meta: {} as any, + statusCode: 404, + warnings: [], + }) + ); + } + } else { + return of(esSearchResponse); + } + }), +}); + +const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => ({ + uiSettingsClient: uiSettingsServiceMock.createClient(), + esClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), +}); + +// using the official data mock from within x-pack doesn't type-check successfully, +// because the `licensing` plugin modifies the `RequestHandlerContext` core type. +const createDataPluginMock = (esSearchStrategyMock: ISearchStrategy): any => ({ + search: { + getSearchStrategy: jest.fn().mockReturnValue(esSearchStrategyMock), + }, +}); diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts new file mode 100644 index 0000000000000..a0dfe3d7176fd --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { concat, defer, of } from 'rxjs'; +import { concatMap, filter, map, shareReplay, take } from 'rxjs/operators'; +import type { + IEsSearchRequest, + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../../src/plugins/data/common'; +import type { + ISearchStrategy, + PluginStart as DataPluginStart, +} from '../../../../../../src/plugins/data/server'; +import { getLogEntryCursorFromHit } from '../../../common/log_entry'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { + LogEntrySearchRequestParams, + logEntrySearchRequestParamsRT, + LogEntrySearchResponsePayload, + logEntrySearchResponsePayloadRT, +} from '../../../common/search_strategies/log_entries/log_entry'; +import type { IInfraSources } from '../../lib/sources'; +import { + createAsyncRequestRTs, + createErrorFromShardFailure, + jsonFromBase64StringRT, +} from '../../utils/typed_search_strategy'; +import { createGetLogEntryQuery, getLogEntryResponseRT, LogEntryHit } from './queries/log_entry'; + +type LogEntrySearchRequest = IKibanaSearchRequest; +type LogEntrySearchResponse = IKibanaSearchResponse; + +export const logEntrySearchStrategyProvider = ({ + data, + sources, +}: { + data: DataPluginStart; + sources: IInfraSources; +}): ISearchStrategy => { + const esSearchStrategy = data.search.getSearchStrategy('ese'); + + return { + search: (rawRequest, options, dependencies) => + defer(() => { + const request = decodeOrThrow(asyncRequestRT)(rawRequest); + + const sourceConfiguration$ = defer(() => + sources.getSourceConfiguration(dependencies.savedObjectsClient, request.params.sourceId) + ).pipe(shareReplay(1)); + + const recoveredRequest$ = of(request).pipe( + filter(asyncRecoveredRequestRT.is), + map(({ id: { esRequestId } }) => ({ id: esRequestId })) + ); + + const initialRequest$ = of(request).pipe( + filter(asyncInitialRequestRT.is), + concatMap(({ params }) => + sourceConfiguration$.pipe( + map( + ({ configuration }): IEsSearchRequest => ({ + params: createGetLogEntryQuery( + configuration.logAlias, + params.logEntryId, + configuration.fields.timestamp, + configuration.fields.tiebreaker + ), + }) + ) + ) + ) + ); + + return concat(recoveredRequest$, initialRequest$).pipe( + take(1), + concatMap((esRequest) => esSearchStrategy.search(esRequest, options, dependencies)), + map((esResponse) => ({ + ...esResponse, + rawResponse: decodeOrThrow(getLogEntryResponseRT)(esResponse.rawResponse), + })), + map((esResponse) => ({ + ...esResponse, + ...(esResponse.id + ? { id: logEntrySearchRequestStateRT.encode({ esRequestId: esResponse.id }) } + : {}), + rawResponse: logEntrySearchResponsePayloadRT.encode({ + data: esResponse.rawResponse.hits.hits.map(createLogEntryFromHit)[0] ?? null, + errors: (esResponse.rawResponse._shards.failures ?? []).map( + createErrorFromShardFailure + ), + }), + })) + ); + }), + cancel: async (id, options, dependencies) => { + const { esRequestId } = decodeOrThrow(logEntrySearchRequestStateRT)(id); + return await esSearchStrategy.cancel?.(esRequestId, options, dependencies); + }, + }; +}; + +// exported for tests +export const logEntrySearchRequestStateRT = rt.string.pipe(jsonFromBase64StringRT).pipe( + rt.type({ + esRequestId: rt.string, + }) +); + +const { asyncInitialRequestRT, asyncRecoveredRequestRT, asyncRequestRT } = createAsyncRequestRTs( + logEntrySearchRequestStateRT, + logEntrySearchRequestParamsRT +); + +const createLogEntryFromHit = (hit: LogEntryHit) => ({ + id: hit._id, + index: hit._index, + key: getLogEntryCursorFromHit(hit), + fields: Object.entries(hit.fields).map(([field, value]) => ({ field, value })), +}); diff --git a/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts b/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts new file mode 100644 index 0000000000000..880a48fd5b8f7 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts @@ -0,0 +1,57 @@ +/* + * 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 type { RequestParams } from '@elastic/elasticsearch'; +import * as rt from 'io-ts'; +import { jsonArrayRT } from '../../../../common/typed_json'; +import { + commonHitFieldsRT, + commonSearchSuccessResponseFieldsRT, +} from '../../../utils/elasticsearch_runtime_types'; + +export const createGetLogEntryQuery = ( + logEntryIndex: string, + logEntryId: string, + timestampField: string, + tiebreakerField: string +): RequestParams.Search> => ({ + index: logEntryIndex, + terminate_after: 1, + track_scores: false, + track_total_hits: false, + body: { + size: 1, + query: { + ids: { + values: [logEntryId], + }, + }, + fields: ['*'], + sort: [{ [timestampField]: 'desc' }, { [tiebreakerField]: 'desc' }], + _source: false, + }, +}); + +export const logEntryHitRT = rt.intersection([ + commonHitFieldsRT, + rt.type({ + fields: rt.record(rt.string, jsonArrayRT), + sort: rt.tuple([rt.number, rt.number]), + }), +]); + +export type LogEntryHit = rt.TypeOf; + +export const getLogEntryResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + hits: rt.type({ + hits: rt.array(logEntryHitRT), + }), + }), +]); + +export type GetLogEntryResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/server/services/log_entries/types.ts b/x-pack/plugins/infra/server/services/log_entries/types.ts new file mode 100644 index 0000000000000..d9f1024845bad --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_entries/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../../../src/plugins/data/server'; +import { InfraSources } from '../../lib/sources'; + +export interface LogEntriesServiceSetupDeps { + data: DataPluginSetup; + sources: InfraSources; +} + +export interface LogEntriesServiceStartDeps { + data: DataPluginStart; +} diff --git a/x-pack/plugins/infra/server/utils/elasticsearch_runtime_types.ts b/x-pack/plugins/infra/server/utils/elasticsearch_runtime_types.ts index a48c65d648b25..271dbb864abad 100644 --- a/x-pack/plugins/infra/server/utils/elasticsearch_runtime_types.ts +++ b/x-pack/plugins/infra/server/utils/elasticsearch_runtime_types.ts @@ -6,13 +6,35 @@ import * as rt from 'io-ts'; -export const commonSearchSuccessResponseFieldsRT = rt.type({ - _shards: rt.type({ - total: rt.number, - successful: rt.number, - skipped: rt.number, - failed: rt.number, +export const shardFailureRT = rt.type({ + index: rt.string, + node: rt.string, + reason: rt.type({ + reason: rt.string, + type: rt.string, }), + shard: rt.number, +}); + +export type ShardFailure = rt.TypeOf; + +export const commonSearchSuccessResponseFieldsRT = rt.type({ + _shards: rt.intersection([ + rt.type({ + total: rt.number, + successful: rt.number, + skipped: rt.number, + failed: rt.number, + }), + rt.partial({ + failures: rt.array(shardFailureRT), + }), + ]), timed_out: rt.boolean, took: rt.number, }); + +export const commonHitFieldsRT = rt.type({ + _index: rt.string, + _id: rt.string, +}); diff --git a/x-pack/plugins/infra/server/utils/typed_search_strategy.ts b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts new file mode 100644 index 0000000000000..1234aea507f3f --- /dev/null +++ b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts @@ -0,0 +1,57 @@ +/* + * 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 rt from 'io-ts'; +import stringify from 'json-stable-stringify'; +import { JsonValue, jsonValueRT } from '../../common/typed_json'; +import { SearchStrategyError } from '../../common/search_strategies/common/errors'; +import { ShardFailure } from './elasticsearch_runtime_types'; + +export const jsonFromBase64StringRT = new rt.Type( + 'JSONFromBase64String', + jsonValueRT.is, + (value, context) => { + try { + return rt.success(JSON.parse(Buffer.from(value, 'base64').toString())); + } catch (error) { + return rt.failure(error, context); + } + }, + (a) => Buffer.from(stringify(a)).toString('base64') +); + +export const createAsyncRequestRTs = ( + stateCodec: StateCodec, + paramsCodec: ParamsCodec +) => { + const asyncRecoveredRequestRT = rt.type({ + id: stateCodec, + params: paramsCodec, + }); + + const asyncInitialRequestRT = rt.type({ + id: rt.undefined, + params: paramsCodec, + }); + + const asyncRequestRT = rt.union([asyncRecoveredRequestRT, asyncInitialRequestRT]); + + return { + asyncInitialRequestRT, + asyncRecoveredRequestRT, + asyncRequestRT, + }; +}; + +export const createErrorFromShardFailure = (failure: ShardFailure): SearchStrategyError => ({ + type: 'shardFailure' as const, + shardInfo: { + index: failure.index, + node: failure.node, + shard: failure.shard, + }, + message: failure.reason.reason, +}); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7b84c62264c83..297ed96a6e494 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9341,7 +9341,6 @@ "xpack.infra.logEntryItemView.logEntryActionsMenuToolTip": "行のアクションを表示", "xpack.infra.logFlyout.fieldColumnLabel": "フィールド", "xpack.infra.logFlyout.filterAriaLabel": "フィルター", - "xpack.infra.logFlyout.flyoutTitle": "ログイベントドキュメントの詳細", "xpack.infra.logFlyout.loadingMessage": "イベントを読み込み中", "xpack.infra.logFlyout.setFilterTooltip": "フィルターでイベントを表示", "xpack.infra.logFlyout.valueColumnLabel": "値", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 55071303a1b36..3e8b5d0996ee7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9350,7 +9350,6 @@ "xpack.infra.logEntryItemView.logEntryActionsMenuToolTip": "查看适用于以下行的操作:", "xpack.infra.logFlyout.fieldColumnLabel": "字段", "xpack.infra.logFlyout.filterAriaLabel": "筛选", - "xpack.infra.logFlyout.flyoutTitle": "日志事件文档详情", "xpack.infra.logFlyout.loadingMessage": "正在加载事件", "xpack.infra.logFlyout.setFilterTooltip": "使用筛选查看事件", "xpack.infra.logFlyout.valueColumnLabel": "值", diff --git a/x-pack/test/api_integration/apis/metrics_ui/index.js b/x-pack/test/api_integration/apis/metrics_ui/index.js index fdd37fa4c335c..819a2d35b92a6 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/index.js +++ b/x-pack/test/api_integration/apis/metrics_ui/index.js @@ -16,7 +16,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./sources')); loadTestFile(require.resolve('./snapshot')); - loadTestFile(require.resolve('./log_item')); loadTestFile(require.resolve('./metrics_alerting')); loadTestFile(require.resolve('./metrics_explorer')); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_item.ts b/x-pack/test/api_integration/apis/metrics_ui/log_item.ts deleted file mode 100644 index 3bb7a9a76690d..0000000000000 --- a/x-pack/test/api_integration/apis/metrics_ui/log_item.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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 '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -import { - LOG_ENTRIES_ITEM_PATH, - logEntriesItemRequestRT, -} from '../../../../plugins/infra/common/http_api'; - -const COMMON_HEADERS = { - 'kbn-xsrf': 'some-xsrf-token', -}; - -export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - - describe('Log Item Endpoint', () => { - before(() => esArchiver.load('infra/metrics_and_logs')); - after(() => esArchiver.unload('infra/metrics_and_logs')); - - it('should basically work', async () => { - const { body } = await supertest - .post(LOG_ENTRIES_ITEM_PATH) - .set(COMMON_HEADERS) - .send( - logEntriesItemRequestRT.encode({ - sourceId: 'default', - id: 'yT2Mg2YBh-opCxJv8Vqj', - }) - ) - .expect(200); - - const logItem = body.data; - - 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'], - }, - { - field: '_id', - value: ['yT2Mg2YBh-opCxJv8Vqj'], - }, - { - field: '_index', - value: ['filebeat-7.0.0-alpha1-2018.10.17'], - }, - { - field: 'apache2.access.body_sent.bytes', - value: ['1336'], - }, - { - field: 'apache2.access.http_version', - value: ['1.1'], - }, - { - field: 'apache2.access.method', - value: ['GET'], - }, - { - field: 'apache2.access.referrer', - value: ['-'], - }, - { - field: 'apache2.access.remote_ip', - value: ['10.128.0.11'], - }, - { - field: 'apache2.access.response_code', - value: ['200'], - }, - { - field: 'apache2.access.url', - value: ['/a-fresh-start-will-put-you-on-your-way'], - }, - { - field: 'apache2.access.user_agent.device', - value: ['Other'], - }, - { - field: 'apache2.access.user_agent.name', - value: ['Other'], - }, - { - field: 'apache2.access.user_agent.os', - value: ['Other'], - }, - { - field: 'apache2.access.user_agent.os_name', - value: ['Other'], - }, - { - field: 'apache2.access.user_name', - value: ['-'], - }, - { - field: 'beat.hostname', - value: ['demo-stack-apache-01'], - }, - { - field: 'beat.name', - value: ['demo-stack-apache-01'], - }, - { - field: 'beat.version', - value: ['7.0.0-alpha1'], - }, - { - field: 'fileset.module', - value: ['apache2'], - }, - { - field: 'fileset.name', - value: ['access'], - }, - { - field: 'host.name', - value: ['demo-stack-apache-01'], - }, - { - field: 'input.type', - value: ['log'], - }, - { - field: 'offset', - value: ['5497614'], - }, - { - field: 'prospector.type', - value: ['log'], - }, - { - field: 'read_timestamp', - value: ['2018-10-17T19:42:23.160Z'], - }, - { - field: 'source', - value: ['/var/log/apache2/access.log'], - }, - ]); - }); - }); -}