diff --git a/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts index 7f8d10b50edf5..96ce7a1fdc097 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/type_registrations.test.ts @@ -56,6 +56,7 @@ const previouslyRegisteredTypes = [ 'fleet-preconfiguration-deletion-record', 'graph-workspace', 'index-pattern', + 'infrastructure-monitoring-log-view', 'infrastructure-ui-source', 'ingest-agent-policies', 'ingest-outputs', diff --git a/x-pack/plugins/infra/.storybook/preview.js b/x-pack/plugins/infra/.storybook/preview.js new file mode 100644 index 0000000000000..59df773136b79 --- /dev/null +++ b/x-pack/plugins/infra/.storybook/preview.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const parameters = { + docs: { + source: { + type: 'code', // without this, stories in mdx documents freeze the browser + }, + }, +}; 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 be3a8f50922f7..ec3a6b8a2d2ec 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 @@ -7,7 +7,7 @@ import * as rt from 'io-ts'; import { logEntryCursorRT, logEntryRT } from '../../log_entry'; -import { logSourceColumnConfigurationRT } from '../../log_sources/log_source_configuration'; +import { logViewColumnConfigurationRT } from '../../log_views'; export const LOG_ENTRIES_HIGHLIGHTS_PATH = '/api/log_entries/highlights'; @@ -21,7 +21,7 @@ export const logEntriesHighlightsBaseRequestRT = rt.intersection([ rt.partial({ query: rt.union([rt.string, rt.null]), size: rt.number, - columns: rt.array(logSourceColumnConfigurationRT), + columns: rt.array(logViewColumnConfigurationRT), }), ]); diff --git a/x-pack/plugins/infra/common/http_api/log_sources/common.ts b/x-pack/plugins/infra/common/http_api/log_sources/common.ts deleted file mode 100644 index 3a30e94e9a153..0000000000000 --- a/x-pack/plugins/infra/common/http_api/log_sources/common.ts +++ /dev/null @@ -1,11 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const LOG_SOURCE_CONFIGURATION_PATH_PREFIX = '/api/infra/log_source_configurations'; -export const LOG_SOURCE_CONFIGURATION_PATH = `${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/{sourceId}`; -export const getLogSourceConfigurationPath = (sourceId: string) => - `${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/${sourceId}`; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_configuration.ts deleted file mode 100644 index bbecc642fd7ab..0000000000000 --- a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_configuration.ts +++ /dev/null @@ -1,58 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { badRequestErrorRT, forbiddenErrorRT, routeTimingMetadataRT } from '../shared'; -import { logSourceConfigurationRT } from '../../log_sources/log_source_configuration'; - -/** - * request - */ - -export const getLogSourceConfigurationRequestParamsRT = rt.type({ - // the id of the source configuration - sourceId: rt.string, -}); - -export type GetLogSourceConfigurationRequestParams = rt.TypeOf< - typeof getLogSourceConfigurationRequestParamsRT ->; - -/** - * response - */ - -export const getLogSourceConfigurationSuccessResponsePayloadRT = rt.intersection([ - rt.type({ - data: logSourceConfigurationRT, - }), - rt.partial({ - timing: routeTimingMetadataRT, - }), -]); - -export type GetLogSourceConfigurationSuccessResponsePayload = rt.TypeOf< - typeof getLogSourceConfigurationSuccessResponsePayloadRT ->; - -export const getLogSourceConfigurationErrorResponsePayloadRT = rt.union([ - badRequestErrorRT, - forbiddenErrorRT, -]); - -export type GetLogSourceConfigurationErrorReponsePayload = rt.TypeOf< - typeof getLogSourceConfigurationErrorResponsePayloadRT ->; - -export const getLogSourceConfigurationResponsePayloadRT = rt.union([ - getLogSourceConfigurationSuccessResponsePayloadRT, - getLogSourceConfigurationErrorResponsePayloadRT, -]); - -export type GetLogSourceConfigurationReponsePayload = rt.TypeOf< - typeof getLogSourceConfigurationResponsePayloadRT ->; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts deleted file mode 100644 index dafc904b93b1d..0000000000000 --- a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts +++ /dev/null @@ -1,67 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { routeTimingMetadataRT } from '../shared'; -import { getLogSourceConfigurationPath, LOG_SOURCE_CONFIGURATION_PATH } from './common'; - -export const LOG_SOURCE_STATUS_PATH_SUFFIX = 'status'; -export const LOG_SOURCE_STATUS_PATH = `${LOG_SOURCE_CONFIGURATION_PATH}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`; -export const getLogSourceStatusPath = (sourceId: string) => - `${getLogSourceConfigurationPath(sourceId)}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`; - -/** - * request - */ - -export const getLogSourceStatusRequestParamsRT = rt.type({ - // the id of the source configuration - sourceId: rt.string, -}); - -export type GetLogSourceStatusRequestParams = rt.TypeOf; - -/** - * response - */ - -const logIndexFieldRT = rt.strict({ - name: rt.string, - type: rt.string, - searchable: rt.boolean, - aggregatable: rt.boolean, -}); - -export type LogIndexField = rt.TypeOf; - -const logIndexStatusRT = rt.keyof({ - missing: null, - empty: null, - available: null, -}); - -export type LogIndexStatus = rt.TypeOf; - -const logSourceStatusRT = rt.strict({ - logIndexStatus: logIndexStatusRT, - indices: rt.string, -}); - -export type LogSourceStatus = rt.TypeOf; - -export const getLogSourceStatusSuccessResponsePayloadRT = rt.intersection([ - rt.type({ - data: logSourceStatusRT, - }), - rt.partial({ - timing: routeTimingMetadataRT, - }), -]); - -export type GetLogSourceStatusSuccessResponsePayload = rt.TypeOf< - typeof getLogSourceStatusSuccessResponsePayloadRT ->; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts deleted file mode 100644 index a16f0651e7e5d..0000000000000 --- a/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts +++ /dev/null @@ -1,62 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { badRequestErrorRT, forbiddenErrorRT } from '../shared'; -import { getLogSourceConfigurationSuccessResponsePayloadRT } from './get_log_source_configuration'; -import { logSourceConfigurationPropertiesRT } from '../../log_sources/log_source_configuration'; - -/** - * request - */ - -export const patchLogSourceConfigurationRequestParamsRT = rt.type({ - // the id of the source configuration - sourceId: rt.string, -}); - -export type PatchLogSourceConfigurationRequestParams = rt.TypeOf< - typeof patchLogSourceConfigurationRequestParamsRT ->; - -const logSourceConfigurationProperiesPatchRT = rt.partial({ - ...logSourceConfigurationPropertiesRT.type.props, - fields: rt.partial(logSourceConfigurationPropertiesRT.type.props.fields.type.props), -}); - -export type LogSourceConfigurationPropertiesPatch = rt.TypeOf< - typeof logSourceConfigurationProperiesPatchRT ->; - -export const patchLogSourceConfigurationRequestBodyRT = rt.type({ - data: logSourceConfigurationProperiesPatchRT, -}); - -export type PatchLogSourceConfigurationRequestBody = rt.TypeOf< - typeof patchLogSourceConfigurationRequestBodyRT ->; - -/** - * response - */ - -export const patchLogSourceConfigurationSuccessResponsePayloadRT = - getLogSourceConfigurationSuccessResponsePayloadRT; - -export type PatchLogSourceConfigurationSuccessResponsePayload = rt.TypeOf< - typeof patchLogSourceConfigurationSuccessResponsePayloadRT ->; - -export const patchLogSourceConfigurationResponsePayloadRT = rt.union([ - patchLogSourceConfigurationSuccessResponsePayloadRT, - badRequestErrorRT, - forbiddenErrorRT, -]); - -export type PatchLogSourceConfigurationReponsePayload = rt.TypeOf< - typeof patchLogSourceConfigurationResponsePayloadRT ->; diff --git a/x-pack/plugins/infra/common/http_api/log_views/common.ts b/x-pack/plugins/infra/common/http_api/log_views/common.ts new file mode 100644 index 0000000000000..b280fa254ffb6 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_views/common.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const LOG_VIEW_URL_PREFIX = '/api/infra/log_views'; +export const LOG_VIEW_URL = `${LOG_VIEW_URL_PREFIX}/{logViewId}`; +export const getLogViewUrl = (logViewId: string) => `${LOG_VIEW_URL_PREFIX}/${logViewId}`; diff --git a/x-pack/plugins/infra/common/http_api/log_views/get_log_view.ts b/x-pack/plugins/infra/common/http_api/log_views/get_log_view.ts new file mode 100644 index 0000000000000..6135d7b31bc82 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_views/get_log_view.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { logViewRT } from '../../log_views'; + +export const getLogViewRequestParamsRT = rt.type({ + // the id of the log view + logViewId: rt.string, +}); + +export const getLogViewResponsePayloadRT = rt.type({ + data: logViewRT, +}); diff --git a/x-pack/plugins/infra/common/http_api/log_sources/index.ts b/x-pack/plugins/infra/common/http_api/log_views/index.ts similarity index 60% rename from x-pack/plugins/infra/common/http_api/log_sources/index.ts rename to x-pack/plugins/infra/common/http_api/log_views/index.ts index f37ad1f4b1f73..a39f939eb5e98 100644 --- a/x-pack/plugins/infra/common/http_api/log_sources/index.ts +++ b/x-pack/plugins/infra/common/http_api/log_views/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export * from './get_log_source_configuration'; -export * from './get_log_source_status'; -export * from './patch_log_source_configuration'; -export * from './common'; +export { getLogViewUrl, LOG_VIEW_URL } from './common'; +export * from './get_log_view'; +export * from './put_log_view'; diff --git a/x-pack/plugins/infra/common/http_api/log_views/put_log_view.ts b/x-pack/plugins/infra/common/http_api/log_views/put_log_view.ts new file mode 100644 index 0000000000000..22451aa3645c6 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_views/put_log_view.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { logViewAttributesRT, logViewRT } from '../../log_views'; + +export const putLogViewRequestParamsRT = rt.type({ + logViewId: rt.string, +}); + +export const putLogViewRequestPayloadRT = rt.type({ + attributes: rt.partial(logViewAttributesRT.type.props), +}); +export type PutLogViewRequestPayload = rt.TypeOf; + +export const putLogViewResponsePayloadRT = rt.type({ + data: logViewRT, +}); diff --git a/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts deleted file mode 100644 index 5d46ce59457da..0000000000000 --- a/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts +++ /dev/null @@ -1,91 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; - -export const logSourceConfigurationOriginRT = rt.keyof({ - fallback: null, - internal: null, - stored: null, -}); - -export type LogSourceConfigurationOrigin = rt.TypeOf; - -const logSourceFieldsConfigurationRT = rt.strict({ - message: rt.array(rt.string), -}); - -const logSourceCommonColumnConfigurationRT = rt.strict({ - id: rt.string, -}); - -const logSourceTimestampColumnConfigurationRT = rt.strict({ - timestampColumn: logSourceCommonColumnConfigurationRT, -}); - -const logSourceMessageColumnConfigurationRT = rt.strict({ - messageColumn: logSourceCommonColumnConfigurationRT, -}); - -export const logSourceFieldColumnConfigurationRT = rt.strict({ - fieldColumn: rt.intersection([ - logSourceCommonColumnConfigurationRT, - rt.strict({ - field: rt.string, - }), - ]), -}); - -export const logSourceColumnConfigurationRT = rt.union([ - logSourceTimestampColumnConfigurationRT, - logSourceMessageColumnConfigurationRT, - logSourceFieldColumnConfigurationRT, -]); -export type LogSourceColumnConfiguration = rt.TypeOf; - -// Kibana index pattern -export const logIndexPatternReferenceRT = rt.type({ - type: rt.literal('index_pattern'), - indexPatternId: rt.string, -}); -export type LogIndexPatternReference = rt.TypeOf; - -// Legacy support -export const logIndexNameReferenceRT = rt.type({ - type: rt.literal('index_name'), - indexName: rt.string, -}); -export type LogIndexNameReference = rt.TypeOf; - -export const logIndexReferenceRT = rt.union([logIndexPatternReferenceRT, logIndexNameReferenceRT]); -export type LogIndexReference = rt.TypeOf; - -export const logSourceConfigurationPropertiesRT = rt.strict({ - name: rt.string, - description: rt.string, - logIndices: logIndexReferenceRT, - fields: logSourceFieldsConfigurationRT, - logColumns: rt.array(logSourceColumnConfigurationRT), -}); - -export type LogSourceConfigurationProperties = rt.TypeOf; - -export const logSourceConfigurationRT = rt.exact( - rt.intersection([ - rt.type({ - id: rt.string, - origin: logSourceConfigurationOriginRT, - configuration: logSourceConfigurationPropertiesRT, - }), - rt.partial({ - updatedAt: rt.number, - version: rt.string, - }), - ]) -); - -export type LogSourceConfiguration = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts deleted file mode 100644 index 914c55824373a..0000000000000 --- a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts +++ /dev/null @@ -1,109 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/common'; -import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../constants'; -import { ResolveLogSourceConfigurationError } from './errors'; -import { - LogSourceColumnConfiguration, - LogSourceConfigurationProperties, -} from './log_source_configuration'; - -export interface ResolvedLogSourceConfiguration { - name: string; - description: string; - indices: string; - timestampField: string; - tiebreakerField: string; - messageField: string[]; - fields: DataView['fields']; - runtimeMappings: estypes.MappingRuntimeFields; - columns: LogSourceColumnConfiguration[]; -} - -export const resolveLogSourceConfiguration = async ( - sourceConfiguration: LogSourceConfigurationProperties, - indexPatternsService: DataViewsContract -): Promise => { - if (sourceConfiguration.logIndices.type === 'index_name') { - return await resolveLegacyReference(sourceConfiguration, indexPatternsService); - } else { - return await resolveKibanaIndexPatternReference(sourceConfiguration, indexPatternsService); - } -}; - -const resolveLegacyReference = async ( - sourceConfiguration: LogSourceConfigurationProperties, - indexPatternsService: DataViewsContract -): Promise => { - if (sourceConfiguration.logIndices.type !== 'index_name') { - throw new Error('This function can only resolve legacy references'); - } - - const indices = sourceConfiguration.logIndices.indexName; - - const fields = await indexPatternsService - .getFieldsForWildcard({ - pattern: indices, - allowNoIndex: true, - }) - .catch((error) => { - throw new ResolveLogSourceConfigurationError( - `Failed to fetch fields for indices "${indices}": ${error}`, - error - ); - }); - - return { - indices: sourceConfiguration.logIndices.indexName, - timestampField: TIMESTAMP_FIELD, - tiebreakerField: TIEBREAKER_FIELD, - messageField: sourceConfiguration.fields.message, - // @ts-ignore - fields, - runtimeMappings: {}, - columns: sourceConfiguration.logColumns, - name: sourceConfiguration.name, - description: sourceConfiguration.description, - }; -}; - -const resolveKibanaIndexPatternReference = async ( - sourceConfiguration: LogSourceConfigurationProperties, - indexPatternsService: DataViewsContract -): Promise => { - if (sourceConfiguration.logIndices.type !== 'index_pattern') { - throw new Error('This function can only resolve Kibana Index Pattern references'); - } - - const { indexPatternId } = sourceConfiguration.logIndices; - - const indexPattern = await indexPatternsService.get(indexPatternId).catch((error) => { - throw new ResolveLogSourceConfigurationError( - `Failed to fetch index pattern "${indexPatternId}": ${error}`, - error - ); - }); - - return { - indices: indexPattern.title, - timestampField: indexPattern.timeFieldName ?? TIMESTAMP_FIELD, - tiebreakerField: TIEBREAKER_FIELD, - messageField: ['message'], - fields: indexPattern.fields, - runtimeMappings: resolveRuntimeMappings(indexPattern), - columns: sourceConfiguration.logColumns, - name: sourceConfiguration.name, - description: sourceConfiguration.description, - }; -}; - -// this might take other sources of runtime fields into account in the future -const resolveRuntimeMappings = (indexPattern: DataView): estypes.MappingRuntimeFields => { - return indexPattern.getRuntimeMappings(); -}; diff --git a/x-pack/plugins/infra/common/log_views/defaults.ts b/x-pack/plugins/infra/common/log_views/defaults.ts new file mode 100644 index 0000000000000..5ea5207a25c11 --- /dev/null +++ b/x-pack/plugins/infra/common/log_views/defaults.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { defaultSourceConfiguration } from '../source_configuration/defaults'; +import { LogViewAttributes, LogViewsStaticConfig } from './types'; + +export const defaultLogViewId = 'default'; + +export const defaultLogViewAttributes: LogViewAttributes = { + name: 'Log View', + description: 'A default log view', + logIndices: { + type: 'index_name', + indexName: 'logs-*,filebeat-*', + }, + logColumns: [ + { + timestampColumn: { + id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f', + }, + }, + { + fieldColumn: { + id: 'eb9777a8-fcd3-420e-ba7d-172fff6da7a2', + field: 'event.dataset', + }, + }, + { + messageColumn: { + id: 'b645d6da-824b-4723-9a2a-e8cece1645c0', + }, + }, + ], +}; + +export const defaultLogViewsStaticConfig: LogViewsStaticConfig = { + messageFields: defaultSourceConfiguration.fields.message, +}; diff --git a/x-pack/plugins/infra/common/log_sources/errors.ts b/x-pack/plugins/infra/common/log_views/errors.ts similarity index 65% rename from x-pack/plugins/infra/common/log_sources/errors.ts rename to x-pack/plugins/infra/common/log_views/errors.ts index d715e8ea616cf..67e5df22406de 100644 --- a/x-pack/plugins/infra/common/log_sources/errors.ts +++ b/x-pack/plugins/infra/common/log_views/errors.ts @@ -7,34 +7,34 @@ /* eslint-disable max-classes-per-file */ -export class ResolveLogSourceConfigurationError extends Error { +export class ResolveLogViewError extends Error { constructor(message: string, public cause?: Error) { super(message); Object.setPrototypeOf(this, new.target.prototype); - this.name = 'ResolveLogSourceConfigurationError'; + this.name = 'ResolveLogViewError'; } } -export class FetchLogSourceConfigurationError extends Error { +export class FetchLogViewError extends Error { constructor(message: string, public cause?: Error) { super(message); Object.setPrototypeOf(this, new.target.prototype); - this.name = 'FetchLogSourceConfigurationError'; + this.name = 'FetchLogViewError'; } } -export class FetchLogSourceStatusError extends Error { +export class FetchLogViewStatusError extends Error { constructor(message: string, public cause?: Error) { super(message); Object.setPrototypeOf(this, new.target.prototype); - this.name = 'FetchLogSourceStatusError'; + this.name = 'FetchLogViewStatusError'; } } -export class PatchLogSourceConfigurationError extends Error { +export class PutLogViewError extends Error { constructor(message: string, public cause?: Error) { super(message); Object.setPrototypeOf(this, new.target.prototype); - this.name = 'PatchLogSourceConfigurationError'; + this.name = 'PutLogViewError'; } } diff --git a/x-pack/plugins/infra/common/log_sources/index.ts b/x-pack/plugins/infra/common/log_views/index.ts similarity index 74% rename from x-pack/plugins/infra/common/log_sources/index.ts rename to x-pack/plugins/infra/common/log_views/index.ts index a2d200544f45e..dd0cdaece4316 100644 --- a/x-pack/plugins/infra/common/log_sources/index.ts +++ b/x-pack/plugins/infra/common/log_views/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export * from './defaults'; export * from './errors'; -export * from './log_source_configuration'; -export * from './resolved_log_source_configuration'; +export * from './resolved_log_view'; +export * from './types'; diff --git a/x-pack/plugins/infra/common/log_views/log_view.mock.ts b/x-pack/plugins/infra/common/log_views/log_view.mock.ts new file mode 100644 index 0000000000000..37fff51a49b08 --- /dev/null +++ b/x-pack/plugins/infra/common/log_views/log_view.mock.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { defaultLogViewAttributes } from './defaults'; +import { LogView, LogViewAttributes, LogViewOrigin } from './types'; + +export const createLogViewMock = ( + id: string, + origin: LogViewOrigin = 'stored', + attributeOverrides: Partial = {}, + updatedAt?: number, + version?: string +): LogView => ({ + id, + origin, + attributes: { + ...defaultLogViewAttributes, + ...attributeOverrides, + }, + updatedAt, + version, +}); diff --git a/x-pack/plugins/infra/common/log_views/resolved_log_view.mock.ts b/x-pack/plugins/infra/common/log_views/resolved_log_view.mock.ts new file mode 100644 index 0000000000000..a951f88f0c2a5 --- /dev/null +++ b/x-pack/plugins/infra/common/log_views/resolved_log_view.mock.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataViewsContract, fieldList } from 'src/plugins/data_views/common'; +import { createStubDataView } from 'src/plugins/data_views/common/stubs'; +import { defaultLogViewsStaticConfig } from './defaults'; +import { ResolvedLogView, resolveLogView } from './resolved_log_view'; +import { LogViewAttributes } from './types'; + +export const createResolvedLogViewMock = ( + resolvedLogViewOverrides: Partial = {} +): ResolvedLogView => ({ + name: 'LOG VIEW', + description: 'LOG VIEW DESCRIPTION', + indices: 'log-indices-*', + timestampField: 'TIMESTAMP_FIELD', + tiebreakerField: 'TIEBREAKER_FIELD', + messageField: ['MESSAGE_FIELD'], + fields: fieldList(), + runtimeMappings: { + runtime_field: { + type: 'keyword', + script: { + source: 'emit("runtime value")', + }, + }, + }, + columns: [ + { timestampColumn: { id: 'TIMESTAMP_COLUMN_ID' } }, + { + fieldColumn: { + id: 'DATASET_COLUMN_ID', + field: 'event.dataset', + }, + }, + { + messageColumn: { id: 'MESSAGE_COLUMN_ID' }, + }, + ], + ...resolvedLogViewOverrides, +}); + +export const createResolvedLogViewMockFromAttributes = (logViewAttributes: LogViewAttributes) => + resolveLogView( + logViewAttributes, + { + get: async () => createStubDataView({ spec: {} }), + getFieldsForWildcard: async () => [], + } as unknown as DataViewsContract, + defaultLogViewsStaticConfig + ); diff --git a/x-pack/plugins/infra/common/log_views/resolved_log_view.ts b/x-pack/plugins/infra/common/log_views/resolved_log_view.ts new file mode 100644 index 0000000000000..19369a022a923 --- /dev/null +++ b/x-pack/plugins/infra/common/log_views/resolved_log_view.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + DataView, + DataViewsContract, + FieldSpec, +} from '../../../../../src/plugins/data_views/common'; +import { TIEBREAKER_FIELD, TIMESTAMP_FIELD } from '../constants'; +import { ResolveLogViewError } from './errors'; +import { LogViewAttributes, LogViewColumnConfiguration, LogViewsStaticConfig } from './types'; + +export type ResolvedLogViewField = FieldSpec; + +export interface ResolvedLogView { + name: string; + description: string; + indices: string; + timestampField: string; + tiebreakerField: string; + messageField: string[]; + fields: ResolvedLogViewField[]; + runtimeMappings: estypes.MappingRuntimeFields; + columns: LogViewColumnConfiguration[]; +} + +export const resolveLogView = async ( + logViewAttributes: LogViewAttributes, + dataViewsService: DataViewsContract, + config: LogViewsStaticConfig +): Promise => { + if (logViewAttributes.logIndices.type === 'index_name') { + return await resolveLegacyReference(logViewAttributes, dataViewsService, config); + } else { + return await resolveDataViewReference(logViewAttributes, dataViewsService); + } +}; + +const resolveLegacyReference = async ( + logViewAttributes: LogViewAttributes, + dataViewsService: DataViewsContract, + config: LogViewsStaticConfig +): Promise => { + if (logViewAttributes.logIndices.type !== 'index_name') { + throw new Error('This function can only resolve legacy references'); + } + + const indices = logViewAttributes.logIndices.indexName; + + const fields = await dataViewsService + .getFieldsForWildcard({ + pattern: indices, + allowNoIndex: true, + }) + .catch((error) => { + throw new ResolveLogViewError( + `Failed to fetch fields for indices "${indices}": ${error}`, + error + ); + }); + + return { + indices: logViewAttributes.logIndices.indexName, + timestampField: TIMESTAMP_FIELD, + tiebreakerField: TIEBREAKER_FIELD, + messageField: config.messageFields, + fields, + runtimeMappings: {}, + columns: logViewAttributes.logColumns, + name: logViewAttributes.name, + description: logViewAttributes.description, + }; +}; + +const resolveDataViewReference = async ( + logViewAttributes: LogViewAttributes, + dataViewsService: DataViewsContract +): Promise => { + if (logViewAttributes.logIndices.type !== 'data_view') { + throw new Error('This function can only resolve Kibana data view references'); + } + + const { dataViewId } = logViewAttributes.logIndices; + + const dataView = await dataViewsService.get(dataViewId).catch((error) => { + throw new ResolveLogViewError(`Failed to fetch data view "${dataViewId}": ${error}`, error); + }); + + return { + indices: dataView.title, + timestampField: dataView.timeFieldName ?? TIMESTAMP_FIELD, + tiebreakerField: TIEBREAKER_FIELD, + messageField: ['message'], + fields: dataView.fields, + runtimeMappings: resolveRuntimeMappings(dataView), + columns: logViewAttributes.logColumns, + name: logViewAttributes.name, + description: logViewAttributes.description, + }; +}; + +// this might take other sources of runtime fields into account in the future +const resolveRuntimeMappings = (dataView: DataView): estypes.MappingRuntimeFields => { + return dataView.getRuntimeMappings(); +}; diff --git a/x-pack/plugins/infra/common/log_views/types.ts b/x-pack/plugins/infra/common/log_views/types.ts new file mode 100644 index 0000000000000..c8dd7f5174657 --- /dev/null +++ b/x-pack/plugins/infra/common/log_views/types.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export interface LogViewsStaticConfig { + messageFields: string[]; +} + +export const logViewOriginRT = rt.keyof({ + stored: null, + internal: null, + 'infra-source-stored': null, + 'infra-source-internal': null, + 'infra-source-fallback': null, +}); +export type LogViewOrigin = rt.TypeOf; + +// Kibana data views +export const logDataViewReferenceRT = rt.type({ + type: rt.literal('data_view'), + dataViewId: rt.string, +}); + +export type LogDataViewReference = rt.TypeOf; + +// Index name +export const logIndexNameReferenceRT = rt.type({ + type: rt.literal('index_name'), + indexName: rt.string, +}); +export type LogIndexNameReference = rt.TypeOf; + +export const logIndexReferenceRT = rt.union([logDataViewReferenceRT, logIndexNameReferenceRT]); +export type LogIndexReference = rt.TypeOf; + +const logViewCommonColumnConfigurationRT = rt.strict({ + id: rt.string, +}); + +const logViewTimestampColumnConfigurationRT = rt.strict({ + timestampColumn: logViewCommonColumnConfigurationRT, +}); + +const logViewMessageColumnConfigurationRT = rt.strict({ + messageColumn: logViewCommonColumnConfigurationRT, +}); + +export const logViewFieldColumnConfigurationRT = rt.strict({ + fieldColumn: rt.intersection([ + logViewCommonColumnConfigurationRT, + rt.strict({ + field: rt.string, + }), + ]), +}); + +export const logViewColumnConfigurationRT = rt.union([ + logViewTimestampColumnConfigurationRT, + logViewMessageColumnConfigurationRT, + logViewFieldColumnConfigurationRT, +]); +export type LogViewColumnConfiguration = rt.TypeOf; + +export const logViewAttributesRT = rt.strict({ + name: rt.string, + description: rt.string, + logIndices: logIndexReferenceRT, + logColumns: rt.array(logViewColumnConfigurationRT), +}); +export type LogViewAttributes = rt.TypeOf; + +export const logViewRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + origin: logViewOriginRT, + attributes: logViewAttributesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); +export type LogView = rt.TypeOf; + +export const logViewIndexStatusRT = rt.keyof({ + available: null, + empty: null, + missing: null, + unknown: null, +}); +export type LogViewIndexStatus = rt.TypeOf; + +export const logViewStatusRT = rt.strict({ + index: logViewIndexStatusRT, +}); +export type LogViewStatus = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/plugin_config_types.ts b/x-pack/plugins/infra/common/plugin_config_types.ts new file mode 100644 index 0000000000000..59ed36c9b3279 --- /dev/null +++ b/x-pack/plugins/infra/common/plugin_config_types.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface InfraConfig { + alerting: { + inventory_threshold: { + group_by_page_size: number; + }; + metric_threshold: { + group_by_page_size: number; + }; + }; + inventory: { + compositeSize: number; + }; + sources?: { + default?: { + fields?: { + message?: string[]; + }; + }; + }; +} + +export const publicConfigKeys = { + sources: true, +} as const; + +export type InfraPublicConfigKey = keyof { + [K in keyof typeof publicConfigKeys as typeof publicConfigKeys[K] extends true ? K : never]: true; +}; + +export type InfraPublicConfig = Pick; diff --git a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts index 4e115cda6a8e6..65bcec8c98e6a 100644 --- a/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts +++ b/x-pack/plugins/infra/common/search_strategies/log_entries/log_entries.ts @@ -5,15 +5,15 @@ * 2.0. */ -import * as rt from 'io-ts'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { logSourceColumnConfigurationRT } from '../../log_sources/log_source_configuration'; +import * as rt from 'io-ts'; import { logEntryAfterCursorRT, logEntryBeforeCursorRT, logEntryCursorRT, logEntryRT, } from '../../log_entry'; +import { logViewColumnConfigurationRT } from '../../log_views'; import { jsonObjectRT } from '../../typed_json'; import { searchStrategyErrorRT } from '../common/errors'; @@ -28,7 +28,7 @@ const logEntriesBaseSearchRequestParamsRT = rt.intersection([ }), rt.partial({ query: jsonObjectRT, - columns: rt.array(logSourceColumnConfigurationRT), + columns: rt.array(logViewColumnConfigurationRT), highlightPhrase: rt.string, }), ]); diff --git a/x-pack/plugins/infra/common/source_configuration/defaults.ts b/x-pack/plugins/infra/common/source_configuration/defaults.ts new file mode 100644 index 0000000000000..a18cec903b60b --- /dev/null +++ b/x-pack/plugins/infra/common/source_configuration/defaults.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LOGS_INDEX_PATTERN, METRICS_INDEX_PATTERN } from '../constants'; +import { InfraSourceConfiguration } from './source_configuration'; + +export const defaultSourceConfiguration: InfraSourceConfiguration = { + name: 'Default', + description: '', + metricAlias: METRICS_INDEX_PATTERN, + logIndices: { + type: 'index_name', + indexName: LOGS_INDEX_PATTERN, + }, + fields: { + message: ['message', '@message'], + }, + inventoryDefaultView: '0', + metricsExplorerDefaultView: '0', + logColumns: [ + { + timestampColumn: { + id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f', + }, + }, + { + fieldColumn: { + id: ' eb9777a8-fcd3-420e-ba7d-172fff6da7a2', + field: 'event.dataset', + }, + }, + { + messageColumn: { + id: 'b645d6da-824b-4723-9a2a-e8cece1645c0', + }, + }, + ], + anomalyThreshold: 50, +}; diff --git a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts index 0c30c3d678b2a..f6ea22792e48c 100644 --- a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts +++ b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts @@ -22,7 +22,6 @@ import * as rt from 'io-ts'; import moment from 'moment'; import { pipe } from 'fp-ts/lib/pipeable'; import { chain } from 'fp-ts/lib/Either'; -import { logIndexReferenceRT } from '../log_sources'; export const TimestampFromString = new rt.Type( 'TimestampFromString', @@ -103,6 +102,27 @@ export const SourceConfigurationColumnRuntimeType = rt.union([ export type InfraSourceConfigurationColumn = rt.TypeOf; +/** + * Log indices + */ + +// Kibana index pattern +export const logIndexPatternReferenceRT = rt.type({ + type: rt.literal('index_pattern'), + indexPatternId: rt.string, +}); +export type LogIndexPatternReference = rt.TypeOf; + +// Legacy support +export const logIndexNameReferenceRT = rt.type({ + type: rt.literal('index_name'), + indexName: rt.string, +}); +export type LogIndexNameReference = rt.TypeOf; + +export const logIndexReferenceRT = rt.union([logIndexPatternReferenceRT, logIndexNameReferenceRT]); +export type LogIndexReference = rt.TypeOf; + /** * Fields */ diff --git a/x-pack/plugins/infra/common/utility_types.ts b/x-pack/plugins/infra/common/utility_types.ts index 49d60c31a71ef..8c7280ff54a85 100644 --- a/x-pack/plugins/infra/common/utility_types.ts +++ b/x-pack/plugins/infra/common/utility_types.ts @@ -48,3 +48,5 @@ export type ObjectValues = Array; export type ObjectEntry = [keyof T, T[keyof T]]; export type ObjectEntries = Array>; + +export type UnwrapPromise> = T extends Promise ? Value : never; diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 833183ae88276..c70099e331492 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -10,6 +10,7 @@ "embeddable", "data", "dataEnhanced", + "dataViews", "visTypeTimeseries", "alerting", "triggersActionsUi", diff --git a/x-pack/plugins/infra/public/alerting/common/group_by_expression/group_by_expression.tsx b/x-pack/plugins/infra/public/alerting/common/group_by_expression/group_by_expression.tsx index 02bc04e29f970..5a162ef1d6941 100644 --- a/x-pack/plugins/infra/public/alerting/common/group_by_expression/group_by_expression.tsx +++ b/x-pack/plugins/infra/public/alerting/common/group_by_expression/group_by_expression.tsx @@ -5,21 +5,21 @@ * 2.0. */ -import React, { useState, useMemo } from 'react'; -import { DataViewField } from 'src/plugins/data_views/common'; -import { i18n } from '@kbn/i18n'; import { - EuiPopoverTitle, - EuiFlexItem, + EuiExpression, EuiFlexGroup, + EuiFlexItem, EuiPopover, - EuiExpression, + EuiPopoverTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo, useState } from 'react'; +import { FieldSpec } from 'src/plugins/data_views/common'; import { GroupBySelector } from './selector'; interface Props { selectedGroups?: string[]; - fields: DataViewField[]; + fields: FieldSpec[]; onChange: (groupBy: string[]) => void; label?: string; } diff --git a/x-pack/plugins/infra/public/alerting/common/group_by_expression/selector.tsx b/x-pack/plugins/infra/public/alerting/common/group_by_expression/selector.tsx index c1426908e40f8..4e5a574d67e9b 100644 --- a/x-pack/plugins/infra/public/alerting/common/group_by_expression/selector.tsx +++ b/x-pack/plugins/infra/public/alerting/common/group_by_expression/selector.tsx @@ -7,12 +7,12 @@ import { EuiComboBox } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; -import { DataViewField } from 'src/plugins/data_views/common'; +import { FieldSpec } from 'src/plugins/data_views/common'; interface Props { selectedGroups?: string[]; onChange: (groupBy: string[]) => void; - fields: DataViewField[]; + fields: FieldSpec[]; label: string; placeholder: string; } diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx index a746fc5371af0..311df76ce7400 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criteria.tsx @@ -9,7 +9,7 @@ import React, { useCallback } from 'react'; import { EuiFlexItem, EuiFlexGroup, EuiButtonEmpty, EuiAccordion, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { DataViewField } from 'src/plugins/data_views/common'; +import type { ResolvedLogViewField } from '../../../../../common/log_views'; import { Criterion } from './criterion'; import { PartialRuleParams, @@ -34,7 +34,7 @@ const QueryBText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCrit }); interface SharedProps { - fields: DataViewField[]; + fields: ResolvedLogViewField[]; criteria?: PartialCriteriaType; defaultCriterion: PartialCriterionType; errors: Errors['criteria']; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx index b257c54765a75..2c279f6413353 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion.tsx @@ -5,30 +5,29 @@ * 2.0. */ -import React, { useState, useMemo, useCallback } from 'react'; import { - EuiPopoverTitle, - EuiFlexItem, - EuiFlexGroup, - EuiPopover, - EuiSelect, - EuiFieldNumber, + EuiButtonIcon, + EuiComboBox, EuiExpression, + EuiFieldNumber, EuiFieldText, - EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, EuiFormRow, - EuiComboBox, + EuiPopover, + EuiPopoverTitle, + EuiSelect, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DataViewField } from 'src/plugins/data_views/common'; -import { isNumber, isFinite } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { isFinite, isNumber } from 'lodash'; +import React, { useCallback, useMemo, useState } from 'react'; +import type { IErrorObject } from '../../../../../../triggers_actions_ui/public'; import { Comparator, - Criterion as CriterionType, ComparatorToi18nMap, + Criterion as CriterionType, } from '../../../../../common/alerting/logs/log_threshold/types'; +import type { ResolvedLogViewField } from '../../../../../common/log_views'; const firstCriterionFieldPrefix = i18n.translate( 'xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix', @@ -55,7 +54,7 @@ const criterionComparatorValueTitle = i18n.translate( } ); -const getCompatibleComparatorsForField = (fieldInfo: DataViewField | undefined) => { +const getCompatibleComparatorsForField = (fieldInfo: ResolvedLogViewField | undefined) => { if (fieldInfo?.type === 'number') { return [ { value: Comparator.GT, text: ComparatorToi18nMap[Comparator.GT] }, @@ -83,7 +82,10 @@ const getCompatibleComparatorsForField = (fieldInfo: DataViewField | undefined) } }; -const getFieldInfo = (fields: DataViewField[], fieldName: string): DataViewField | undefined => { +const getFieldInfo = ( + fields: ResolvedLogViewField[], + fieldName: string +): ResolvedLogViewField | undefined => { return fields.find((field) => { return field.name === fieldName; }); @@ -91,7 +93,7 @@ const getFieldInfo = (fields: DataViewField[], fieldName: string): DataViewField interface Props { idx: number; - fields: DataViewField[]; + fields: ResolvedLogViewField[]; criterion: Partial; updateCriterion: (idx: number, params: Partial) => void; removeCriterion: (idx: number) => void; @@ -117,7 +119,7 @@ export const Criterion: React.FC = ({ }); }, [fields]); - const fieldInfo: DataViewField | undefined = useMemo(() => { + const fieldInfo: ResolvedLogViewField | undefined = useMemo(() => { if (criterion.field) { return getFieldInfo(fields, criterion.field); } else { diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx index bdd6961ec86f2..920c3cbf1e873 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/editor.tsx @@ -9,30 +9,27 @@ import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eu import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo, useState } from 'react'; import useMount from 'react-use/lib/useMount'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ResolvedLogViewField } from '../../../../../common/log_views'; import { - RuleTypeParamsExpressionProps, ForLastExpression, + RuleTypeParamsExpressionProps, } from '../../../../../../triggers_actions_ui/public'; import { Comparator, + isOptimizableGroupedThreshold, isRatioRule, - PartialRuleParams, PartialCountRuleParams, PartialCriteria as PartialCriteriaType, PartialRatioRuleParams, + PartialRuleParams, ThresholdType, timeUnitRT, - isOptimizableGroupedThreshold, } from '../../../../../common/alerting/logs/log_threshold/types'; import { decodeOrThrow } from '../../../../../common/runtime_types'; import { ObjectEntries } from '../../../../../common/utility_types'; -import { - LogIndexField, - LogSourceProvider, - useLogSourceContext, -} from '../../../../containers/logs/log_source'; import { useSourceId } from '../../../../containers/source_id'; +import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; +import { LogViewProvider, useLogViewContext } from '../../../../hooks/use_log_view'; import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression'; import { errorsRT } from '../../validation'; import { Criteria } from './criteria'; @@ -57,7 +54,7 @@ const DEFAULT_BASE_EXPRESSION = { const DEFAULT_FIELD = 'log.level'; const createDefaultCriterion = ( - availableFields: LogIndexField[], + availableFields: ResolvedLogViewField[], value: ExpressionCriteria['value'] ) => availableFields.some((availableField) => availableField.name === DEFAULT_FIELD) @@ -65,7 +62,7 @@ const createDefaultCriterion = ( : { field: undefined, comparator: undefined, value: undefined }; const createDefaultCountRuleParams = ( - availableFields: LogIndexField[] + availableFields: ResolvedLogViewField[] ): PartialCountRuleParams => ({ ...DEFAULT_BASE_EXPRESSION, count: { @@ -76,7 +73,7 @@ const createDefaultCountRuleParams = ( }); const createDefaultRatioRuleParams = ( - availableFields: LogIndexField[] + availableFields: ResolvedLogViewField[] ): PartialRatioRuleParams => ({ ...DEFAULT_BASE_EXPRESSION, count: { @@ -93,8 +90,10 @@ export const ExpressionEditor: React.FC< RuleTypeParamsExpressionProps > = (props) => { const isInternal = props.metadata?.isInternal ?? false; - const [sourceId] = useSourceId(); - const { http } = useKibana().services; + const [logViewId] = useSourceId(); + const { + services: { http, logViews }, + } = useKibanaContextForPlugin(); // injected during alert registration return ( <> @@ -103,42 +102,28 @@ export const ExpressionEditor: React.FC< ) : ( - + - + )} ); }; export const SourceStatusWrapper: React.FC = ({ children }) => { - const { - initialize, - loadSource, - isLoadingSourceConfiguration, - hasFailedLoadingSource, - isUninitialized, - } = useLogSourceContext(); - - useMount(() => { - initialize(); - }); + const { load, isLoading, hasFailedLoading, isUninitialized } = useLogViewContext(); return ( <> - {isLoadingSourceConfiguration || isUninitialized ? ( + {isLoading || isUninitialized ? (
- ) : hasFailedLoadingSource ? ( + ) : hasFailedLoading ? ( { color="danger" iconType="alert" > - + {i18n.translate('xpack.infra.logs.alertFlyout.sourceStatusErrorTryAgain', { defaultMessage: 'Try again', })} @@ -164,7 +149,7 @@ export const Editor: React.FC { const { setRuleParams, ruleParams, errors } = props; const [hasSetDefaults, setHasSetDefaults] = useState(false); - const { sourceId, resolvedSourceConfiguration } = useLogSourceContext(); + const { logViewId, resolvedLogView } = useLogViewContext(); const { criteria: criteriaErrors, @@ -174,24 +159,24 @@ export const Editor: React.FC decodeOrThrow(errorsRT)(errors), [errors]); const supportedFields = useMemo(() => { - if (resolvedSourceConfiguration?.fields) { - return resolvedSourceConfiguration.fields.filter((field) => { + if (resolvedLogView?.fields) { + return resolvedLogView.fields.filter((field) => { return (field.type === 'string' || field.type === 'number') && field.searchable; }); } else { return []; } - }, [resolvedSourceConfiguration]); + }, [resolvedLogView]); const groupByFields = useMemo(() => { - if (resolvedSourceConfiguration?.fields) { - return resolvedSourceConfiguration.fields.filter((field) => { + if (resolvedLogView?.fields) { + return resolvedLogView.fields.filter((field) => { return field.type === 'string' && field.aggregatable; }); } else { return []; } - }, [resolvedSourceConfiguration]); + }, [resolvedLogView]); const updateThreshold = useCallback( (thresholdParams) => { @@ -276,7 +261,7 @@ export const Editor: React.FC ) : null; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx similarity index 80% rename from x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts rename to x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx index a6c1eaaa07e1b..7d89dac681743 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx @@ -6,16 +6,24 @@ */ import { i18n } from '@kbn/i18n'; -import React from 'react'; import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID, PartialRuleParams, } from '../../../common/alerting/logs/log_threshold'; +import { createLazyComponentWithKibanaContext } from '../../hooks/use_kibana'; +import { InfraClientCoreSetup } from '../../types'; import { formatRuleData } from './rule_data_formatters'; import { validateExpression } from './validation'; -export function createLogThresholdRuleType(): ObservabilityRuleTypeModel { +export function createLogThresholdRuleType( + core: InfraClientCoreSetup +): ObservabilityRuleTypeModel { + const ruleParamsExpression = createLazyComponentWithKibanaContext( + core, + () => import('./components/expression_editor/editor') + ); + return { id: LOG_DOCUMENT_COUNT_RULE_TYPE_ID, description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { @@ -25,7 +33,7 @@ export function createLogThresholdRuleType(): ObservabilityRuleTypeModel import('./components/expression_editor/editor')), + ruleParamsExpression, validate: validateExpression, defaultActionMessage: i18n.translate( 'xpack.infra.logs.alerting.threshold.defaultActionMessage', diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index cdfe338fa38b3..2463485137ba3 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -6,7 +6,7 @@ */ import { AppMountParameters, CoreStart } from 'kibana/public'; -import React, { useMemo } from 'react'; +import React from 'react'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider, @@ -14,11 +14,11 @@ import { useUiSetting$, } from '../../../../../src/plugins/kibana_react/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { NavigationWarningPromptProvider } from '../../../observability/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public'; -import { createKibanaContextForPlugin } from '../hooks/use_kibana'; -import { InfraClientStartDeps } from '../types'; +import { useKibanaContextForPluginProvider } from '../hooks/use_kibana'; +import { InfraClientStartDeps, InfraClientStartExports } from '../types'; import { HeaderActionMenuProvider } from '../utils/header_action_menu_provider'; -import { NavigationWarningPromptProvider } from '../../../observability/public'; import { TriggersActionsProvider } from '../utils/triggers_actions_context'; export const CommonInfraProviders: React.FC<{ @@ -45,6 +45,7 @@ export const CommonInfraProviders: React.FC<{ export interface CoreProvidersProps { core: CoreStart; + pluginStart: InfraClientStartExports; plugins: InfraClientStartDeps; theme$: AppMountParameters['theme$']; } @@ -52,16 +53,18 @@ export interface CoreProvidersProps { export const CoreProviders: React.FC = ({ children, core, + pluginStart, plugins, theme$, }) => { - const { Provider: KibanaContextProviderForPlugin } = useMemo( - () => createKibanaContextForPlugin(core, plugins), - [core, plugins] + const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider( + core, + plugins, + pluginStart ); return ( - + {children} diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx index 4e9936ad4123f..fd507ef822893 100644 --- a/x-pack/plugins/infra/public/apps/logs_app.tsx +++ b/x-pack/plugins/infra/public/apps/logs_app.tsx @@ -16,13 +16,14 @@ import '../index.scss'; import { NotFoundPage } from '../pages/404'; import { LinkToLogsPage } from '../pages/link_to/link_to_logs'; import { LogsPage } from '../pages/logs'; -import { InfraClientStartDeps } from '../types'; +import { InfraClientStartDeps, InfraClientStartExports } from '../types'; import { CommonInfraProviders, CoreProviders } from './common_providers'; import { prepareMountElement } from './common_styles'; export const renderApp = ( core: CoreStart, plugins: InfraClientStartDeps, + pluginStart: InfraClientStartExports, { element, history, setHeaderActionMenu, theme$ }: AppMountParameters ) => { const storage = new Storage(window.localStorage); @@ -35,6 +36,7 @@ export const renderApp = ( storage={storage} history={history} plugins={plugins} + pluginStart={pluginStart} setHeaderActionMenu={setHeaderActionMenu} theme$={theme$} />, @@ -49,15 +51,16 @@ export const renderApp = ( const LogsApp: React.FC<{ core: CoreStart; history: History; + pluginStart: InfraClientStartExports; plugins: InfraClientStartDeps; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; storage: Storage; theme$: AppMountParameters['theme$']; -}> = ({ core, history, plugins, setHeaderActionMenu, storage, theme$ }) => { +}> = ({ core, history, pluginStart, plugins, setHeaderActionMenu, storage, theme$ }) => { const uiCapabilities = core.application.capabilities; return ( - + { const storage = new Storage(window.localStorage); @@ -35,6 +36,7 @@ export const renderApp = ( core={core} history={history} plugins={plugins} + pluginStart={pluginStart} setHeaderActionMenu={setHeaderActionMenu} storage={storage} theme$={theme$} @@ -50,15 +52,16 @@ export const renderApp = ( const MetricsApp: React.FC<{ core: CoreStart; history: History; + pluginStart: InfraClientStartExports; plugins: InfraClientStartDeps; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; storage: Storage; theme$: AppMountParameters['theme$']; -}> = ({ core, history, plugins, setHeaderActionMenu, storage, theme$ }) => { +}> = ({ core, history, pluginStart, plugins, setHeaderActionMenu, storage, theme$ }) => { const uiCapabilities = core.application.capabilities; return ( - + { describe('createLazyContainerMetricsTable', () => { it('should lazily load and render the table', async () => { - const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock); - const LazyContainerMetricsTable = createLazyContainerMetricsTable(coreProvidersPropsMock); + const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock); + const LazyContainerMetricsTable = createLazyContainerMetricsTable(getStartServices); render(); @@ -62,7 +62,7 @@ describe('ContainerMetricsTable', () => { describe('IntegratedContainerMetricsTable', () => { it('should render a single row of data', async () => { - const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock); + const { coreProvidersPropsMock, fetch } = createStartServicesAccessorMock(fetchMock); const { findByText } = render( import('./integrated_container_metrics_table') ); -export function createLazyContainerMetricsTable(coreProvidersProps: CoreProvidersProps) { +export function createLazyContainerMetricsTable(getStartServices: () => InfraClientStartServices) { return ({ timerange, filterClauseDsl, sourceId, - }: UseNodeMetricsTableOptions & Partial) => ( - - - - ); + }: UseNodeMetricsTableOptions & Partial) => { + const [core, plugins, pluginStart] = getStartServices(); + + return ( + + + + ); + }; } diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/create_lazy_host_metrics_table.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/create_lazy_host_metrics_table.tsx index 39980ebf3604b..7041d7daeeb08 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/create_lazy_host_metrics_table.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/create_lazy_host_metrics_table.tsx @@ -6,24 +6,31 @@ */ import React, { lazy, Suspense } from 'react'; -import type { CoreProvidersProps } from '../../../apps/common_providers'; +import { InfraClientStartServices } from '../../../types'; import type { SourceProviderProps, UseNodeMetricsTableOptions } from '../shared'; const LazyIntegratedHostMetricsTable = lazy(() => import('./integrated_host_metrics_table')); -export function createLazyHostMetricsTable(coreProvidersProps: CoreProvidersProps) { +export function createLazyHostMetricsTable(getStartServices: () => InfraClientStartServices) { return ({ timerange, filterClauseDsl, sourceId, - }: UseNodeMetricsTableOptions & Partial) => ( - - - - ); + }: UseNodeMetricsTableOptions & Partial) => { + const [core, plugins, pluginStart] = getStartServices(); + + return ( + + + + ); + }; } diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.test.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.test.tsx index fd2a010e32321..63b6b87202776 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.test.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/host/host_metrics_table.test.tsx @@ -13,7 +13,7 @@ import type { NodeMetricsTableFetchMock, SourceResponseMock, } from '../test_helpers'; -import { createCoreProvidersPropsMock } from '../test_helpers'; +import { createStartServicesAccessorMock } from '../test_helpers'; import { createLazyHostMetricsTable } from './create_lazy_host_metrics_table'; import IntegratedHostMetricsTable from './integrated_host_metrics_table'; import { metricByField } from './use_host_metrics_table'; @@ -41,8 +41,8 @@ describe('HostMetricsTable', () => { describe('createLazyHostMetricsTable', () => { it('should lazily load and render the table', async () => { - const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock); - const LazyHostMetricsTable = createLazyHostMetricsTable(coreProvidersPropsMock); + const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock); + const LazyHostMetricsTable = createLazyHostMetricsTable(getStartServices); render(); @@ -62,7 +62,7 @@ describe('HostMetricsTable', () => { describe('IntegratedHostMetricsTable', () => { it('should render a single row of data', async () => { - const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock); + const { coreProvidersPropsMock, fetch } = createStartServicesAccessorMock(fetchMock); const { findByText } = render( import('./integrated_pod_metrics_table')); -export function createLazyPodMetricsTable(coreProvidersProps: CoreProvidersProps) { +export function createLazyPodMetricsTable(getStartServices: () => InfraClientStartServices) { return ({ timerange, filterClauseDsl, sourceId, - }: UseNodeMetricsTableOptions & Partial) => ( - - - - ); + }: UseNodeMetricsTableOptions & Partial) => { + const [core, plugins, pluginStart] = getStartServices(); + + return ( + + + + ); + }; } diff --git a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.test.tsx b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.test.tsx index ab4b449f5331b..21d26bd42827c 100644 --- a/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.test.tsx +++ b/x-pack/plugins/infra/public/components/infrastructure_node_metrics_tables/pod/pod_metrics_table.test.tsx @@ -13,7 +13,7 @@ import type { NodeMetricsTableFetchMock, SourceResponseMock, } from '../test_helpers'; -import { createCoreProvidersPropsMock } from '../test_helpers'; +import { createStartServicesAccessorMock } from '../test_helpers'; import { createLazyPodMetricsTable } from './create_lazy_pod_metrics_table'; import IntegratedPodMetricsTable from './integrated_pod_metrics_table'; import { metricByField } from './use_pod_metrics_table'; @@ -41,8 +41,8 @@ describe('PodMetricsTable', () => { describe('createLazyPodMetricsTable', () => { it('should lazily load and render the table', async () => { - const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock); - const LazyPodMetricsTable = createLazyPodMetricsTable(coreProvidersPropsMock); + const { fetch, getStartServices } = createStartServicesAccessorMock(fetchMock); + const LazyPodMetricsTable = createLazyPodMetricsTable(getStartServices); render(); @@ -62,7 +62,7 @@ describe('PodMetricsTable', () => { describe('IntegratedPodMetricsTable', () => { it('should render a single row of data', async () => { - const { coreProvidersPropsMock, fetch } = createCoreProvidersPropsMock(fetchMock); + const { coreProvidersPropsMock, fetch } = createStartServicesAccessorMock(fetchMock); const { findByText } = render( ; export type DataResponseMock = DeepPartial; @@ -20,19 +24,26 @@ export type NodeMetricsTableFetchMock = ( options: HttpFetchOptions ) => Promise; -export function createCoreProvidersPropsMock(fetchMock: NodeMetricsTableFetchMock) { +export function createStartServicesAccessorMock(fetchMock: NodeMetricsTableFetchMock) { const core = coreMock.createStart(); // @ts-expect-error core.http.fetch has overloads, Jest/TypeScript only picks the first definition when mocking core.http.fetch.mockImplementation(fetchMock); const coreProvidersPropsMock: CoreProvidersProps = { core, + pluginStart: {} as InfraClientStartExports, plugins: {} as InfraClientStartDeps, theme$: core.theme.theme$, }; + const getStartServices = (): InfraClientStartServices => [ + coreProvidersPropsMock.core, + coreProvidersPropsMock.plugins, + coreProvidersPropsMock.pluginStart, + ]; return { coreProvidersPropsMock, fetch: core.http.fetch, + getStartServices, }; } diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx index f11430586764d..c442ce31f347a 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx @@ -1,158 +1,6 @@ import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs/blocks'; -import { defer, of, Subject } from 'rxjs'; -import { delay } from 'rxjs/operators'; - -import { I18nProvider } from '@kbn/i18n-react'; -import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { LOG_ENTRIES_SEARCH_STRATEGY } from '../../../common/search_strategies/log_entries/log_entries'; -import { createIndexPatternMock, createIndexPatternsMock } from '../../hooks/use_kibana_index_patterns.mock'; -import { DEFAULT_SOURCE_CONFIGURATION } from '../../test_utils/source_configuration'; -import { generateFakeEntries, ENTRIES_EMPTY } from '../../test_utils/entries'; -import { decorateWithGlobalStorybookThemeProviders } from '../../test_utils/use_global_storybook_theme'; - -import { LogStream } from './'; - - - -export const startTimestamp = 1595145600000; -export const endTimestamp = startTimestamp + 15 * 60 * 1000; - -export const dataMock = { - indexPatterns: createIndexPatternsMock(500, [ - createIndexPatternMock({ - id: 'some-test-id', - title: 'mock-index-pattern-*', - timeFieldName: '@timestamp', - fields: [ - { - name: '@timestamp', - type: KBN_FIELD_TYPES.DATE, - searchable: true, - aggregatable: true, - }, - { - name: 'event.dataset', - type: KBN_FIELD_TYPES.STRING, - searchable: true, - aggregatable: true, - }, - { - name: 'host.name', - type: KBN_FIELD_TYPES.STRING, - searchable: true, - aggregatable: true, - }, - { - name: 'log.level', - type: KBN_FIELD_TYPES.STRING, - searchable: true, - aggregatable: true, - }, - { - name: 'message', - type: KBN_FIELD_TYPES.STRING, - searchable: true, - aggregatable: true, - }, - ], - }) - ]), - search: { - search: ({ params }, options) => { - return defer(() => { - switch (options.strategy) { - case LOG_ENTRIES_SEARCH_STRATEGY: - if (params.after?.time === params.endTimestamp || params.before?.time === params.startTimestamp) { - return of({ - id: 'EMPTY_FAKE_RESPONSE', - total: 1, - loaded: 1, - isRunning: false, - isPartial: false, - rawResponse: ENTRIES_EMPTY, - }); - } else { - const entries = generateFakeEntries( - 200, - params.startTimestamp, - params.endTimestamp, - params.columns || DEFAULT_SOURCE_CONFIGURATION.data.configuration.logColumns - ); - return of({ - id: 'FAKE_RESPONSE', - total: 1, - loaded: 1, - isRunning: false, - isPartial: false, - rawResponse: { - data: { - entries, - topCursor: entries[0].cursor, - bottomCursor: entries[entries.length - 1].cursor, - hasMoreBefore: false, - }, - errors: [], - } - }); - } - default: - return of({ - id: 'FAKE_RESPONSE', - rawResponse: {}, - }); - } - }).pipe(delay(2000)); - }, - }, -}; - - -export const fetch = async function (url, params) { - switch (url) { - case '/api/infra/log_source_configurations/default': - return DEFAULT_SOURCE_CONFIGURATION; - case '/api/infra/log_source_configurations/default/status': - return { - data: { - logIndexStatus: 'available', - } - }; - default: - return {}; - } -}; - -export const uiSettings = { - get: (setting) => { - switch (setting) { - case 'dateFormat': - return 'MMM D, YYYY @ HH:mm:ss.SSS'; - case 'dateFormat:scaled': - return [['', 'HH:mm:ss.SSS']]; - } - }, - get$: () => { - return new Subject(); - }, -}; - -export const Template = (args) => ; - - ( - - - {story()} - - - ), - decorateWithGlobalStorybookThemeProviders, - ]} -/> + + # Embeddable `` component @@ -187,11 +35,7 @@ const startTimestamp = endTimestamp - 15 * 60 * 1000; // 15 minutes This will show a list of log entries between the specified timestamps. - - - {Template.bind({})} - - + ## Query log entries @@ -246,14 +90,7 @@ By default the component will load at the bottom of the list, showing the newest /> ``` - - - {Template.bind({})} - - + ## Highlight a specific entry @@ -263,11 +100,7 @@ The component can highlight a specific line via the `highlight` prop. It takes t ``` - - - {Template.bind({})} - - + ## Column configuration @@ -298,23 +131,7 @@ The easiest way is to specify what columns you want with the `columns` prop. /> ``` - - - {Template.bind({})} - - + The rendering of the column headers and the cell contents can also be customized with the following properties: @@ -389,57 +206,25 @@ The rendering of the column headers and the cell contents can also be customized /> ``` - - { - switch (value) { - case 'debug': - return '🐞'; - case 'info': - return 'ℹ️'; - case 'warn': - return '⚠️'; - case 'error': - return '❌'; - } - }, - }, - { type: 'message' }, - ], - }} - > - {Template.bind({})} - - + -### With a source configuration +### With a static log view configuration -The infra plugin has the concept of a "source configuration", a collection of settings that apply to the logs and metrics UIs. The component uses the source configuration to determine which indices to query or what columns to show. +The infra plugin has the concept of a "log view", a collection of settings that apply to the logs UI. The component uses the log view to determine which indices to query or what columns to show. -The `` component will use the `"default"` source configuration. If you want to use your own configuration, you need to first create it when you initialize your plugin, and then specify it in the `` component with the `sourceId` prop. +The `` component will use the `"default"` log view. If you want to use your own log view, you need to first create it when you initialize your plugin, and then specify it in the `` component with the `sourceId` prop. ```tsx // Your `server/plugin.ts` class MyPlugin { // ... setup(core, plugins) { - plugins.infra.defineInternalSourceConfiguration( - 'my_source', // ID for your source configuration + plugins.infra.logViews.defineInternalLogView( + 'my_log_view', // ID for your log view { name: 'some-name', description: 'some description', - logIndices: { // Also accepts an `index_pattern` type with `indexPatternId` + logIndices: { // Also accepts a `data_view` type with `dataViewId` type: 'index_name', indexName: 'some-index', }, @@ -463,4 +248,4 @@ class MyPlugin { ### Setting component height -It's possible to pass a `height` prop, e.g. `60vh` or `300px`, to specify how much vertical space the component should consume. +It's possible to pass a `height` prop, e.g. `60vh` or `300px`, to specify how much vertical space the component should consume. \ No newline at end of file diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.tsx new file mode 100644 index 0000000000000..d98f9dfd53fe9 --- /dev/null +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { I18nProvider } from '@kbn/i18n-react'; +import type { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { decorateWithGlobalStorybookThemeProviders } from '../../test_utils/use_global_storybook_theme'; +import { LogStream, LogStreamProps } from './log_stream'; +import { decorateWithKibanaContext } from './log_stream.story_decorators'; + +const startTimestamp = 1595145600000; +const endTimestamp = startTimestamp + 15 * 60 * 1000; + +export default { + title: 'infra/LogStream', + component: LogStream, + decorators: [ + (wrappedStory) => {wrappedStory()}, + decorateWithKibanaContext, + decorateWithGlobalStorybookThemeProviders, + ], + parameters: { + layout: 'padded', + }, + args: { + startTimestamp, + endTimestamp, + }, +} as Meta; + +const LogStreamStoryTemplate: Story = (args) => ; + +export const BasicDateRange = LogStreamStoryTemplate.bind({}); + +export const CenteredOnLogEntry = LogStreamStoryTemplate.bind({}); +CenteredOnLogEntry.args = { + center: { time: 1595146275000, tiebreaker: 150 }, +}; + +export const HighlightedLogEntry = LogStreamStoryTemplate.bind({}); +HighlightedLogEntry.args = { + highlight: 'entry-197', +}; + +export const CustomColumns = LogStreamStoryTemplate.bind({}); +CustomColumns.args = { + columns: [ + { type: 'timestamp' }, + { type: 'field', field: 'log.level' }, + { type: 'field', field: 'host.name' }, + { type: 'message' }, + ], +}; + +export const CustomColumnRendering = LogStreamStoryTemplate.bind({}); +CustomColumnRendering.args = { + columns: [ + { type: 'timestamp', header: 'When?' }, + { + type: 'field', + field: 'log.level', + header: false, + width: 24, + render: (value) => { + switch (value) { + case 'debug': + return '🐞'; + case 'info': + return 'ℹ️'; + case 'warn': + return '⚠️'; + case 'error': + return '❌'; + } + }, + }, + { type: 'message' }, + ], +}; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.story_decorators.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.story_decorators.tsx new file mode 100644 index 0000000000000..d8c9b0ac4fac0 --- /dev/null +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.story_decorators.tsx @@ -0,0 +1,151 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { StoryContext } from '@storybook/react'; +import React from 'react'; +import { defer, of, Subject } from 'rxjs'; +import { delay } from 'rxjs/operators'; +import { + ENHANCED_ES_SEARCH_STRATEGY, + ES_SEARCH_STRATEGY, + FieldSpec, +} from '../../../../../../src/plugins/data/common'; +import { + IEsSearchResponse, + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, +} from '../../../../../../src/plugins/data/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { getLogViewResponsePayloadRT } from '../../../common/http_api/log_views'; +import { defaultLogViewAttributes } from '../../../common/log_views'; +import { + LogEntriesSearchResponsePayload, + LOG_ENTRIES_SEARCH_STRATEGY, +} from '../../../common/search_strategies/log_entries/log_entries'; +import { ENTRIES_EMPTY, generateFakeEntries } from '../../test_utils/entries'; + +export const decorateWithKibanaContext = ( + wrappedStory: () => StoryFnReactReturnType, + _storyContext: StoryContext +) => { + const data = { + dataViews: { + getFieldsForWildcard: async (): Promise => { + return []; + }, + }, + search: { + search: ({ params }: IKibanaSearchRequest, options?: ISearchOptions) => { + return defer(() => { + switch (options?.strategy) { + case LOG_ENTRIES_SEARCH_STRATEGY: + if ( + params.after?.time === params.endTimestamp || + params.before?.time === params.startTimestamp + ) { + return of>({ + id: 'MOCK_LOG_ENTRIES_RESPONSE', + total: 1, + loaded: 1, + isRunning: false, + isPartial: false, + rawResponse: ENTRIES_EMPTY, + }); + } else { + const entries = generateFakeEntries( + 200, + params.startTimestamp, + params.endTimestamp, + params.columns || defaultLogViewAttributes.logColumns + ); + return of>({ + id: 'MOCK_LOG_ENTRIES_RESPONSE', + total: 1, + loaded: 1, + isRunning: false, + isPartial: false, + rawResponse: { + data: { + entries, + topCursor: entries[0].cursor, + bottomCursor: entries[entries.length - 1].cursor, + hasMoreBefore: false, + }, + errors: [], + }, + }); + } + case undefined: + case ES_SEARCH_STRATEGY: + case ENHANCED_ES_SEARCH_STRATEGY: + return of({ + id: 'MOCK_INDEX_CHECK_RESPONSE', + total: 1, + loaded: 1, + isRunning: false, + isPartial: false, + rawResponse: { + _shards: { + failed: 0, + successful: 1, + total: 1, + }, + hits: { + hits: [], + total: 1, + }, + timed_out: false, + took: 1, + }, + }); + default: + return of({ + id: 'FAKE_RESPONSE', + rawResponse: {}, + }); + } + }).pipe(delay(2000)); + }, + }, + }; + + const http = { + get: async (path: string) => { + switch (path) { + case '/api/infra/log_views/default': + return getLogViewResponsePayloadRT.encode({ + data: { + id: 'default', + origin: 'stored', + attributes: defaultLogViewAttributes, + }, + }); + default: + return {}; + } + }, + }; + + const uiSettings = { + get: (setting: string) => { + switch (setting) { + case 'dateFormat': + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + case 'dateFormat:scaled': + return [['', 'HH:mm:ss.SSS']]; + } + }, + get$: () => new Subject(), + }; + + return ( + + {wrappedStory()} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx index 02e595628d783..9561e7b684657 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx @@ -13,8 +13,10 @@ import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { LogEntryCursor } from '../../../common/log_entry'; -import { useLogSource } from '../../containers/logs/log_source'; +import { defaultLogViewsStaticConfig } from '../../../common/log_views'; import { BuiltEsQuery, useLogStream } from '../../containers/logs/log_stream'; +import { useLogView } from '../../hooks/use_log_view'; +import { LogViewsClient } from '../../services/log_views'; import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration'; import { useKibanaQuerySettings } from '../../utils/use_kibana_query_settings'; import { ScrollableLogTextStreamView } from '../logging/log_text_stream'; @@ -97,9 +99,10 @@ export const LogStreamContent: React.FC = ({ [columns] ); - // source boilerplate - const { services } = useKibana(); - if (!services?.http?.fetch || !services?.data?.indexPatterns) { + const { + services: { http, data }, + } = useKibana(); + if (http == null || data == null) { throw new Error( ` cannot access kibana core services. @@ -111,32 +114,37 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac const kibanaQuerySettings = useKibanaQuerySettings(); + const logViews = useMemo( + () => new LogViewsClient(data.dataViews, http, data.search.search, defaultLogViewsStaticConfig), + [data.dataViews, data.search.search, http] + ); + const { - derivedIndexPattern, - isLoading: isLoadingSource, - loadSource, - sourceConfiguration, - } = useLogSource({ - sourceId, - fetch: services.http.fetch, - indexPatternsService: services.data.indexPatterns, + derivedDataView, + isLoading: isLoadingLogView, + load: loadLogView, + resolvedLogView, + } = useLogView({ + logViewId: sourceId, + logViews, + fetch: http.fetch, }); const parsedQuery = useMemo(() => { if (typeof query === 'object' && 'bool' in query) { return mergeBoolQueries( query, - buildEsQuery(derivedIndexPattern, [], filters ?? [], kibanaQuerySettings) + buildEsQuery(derivedDataView, [], filters ?? [], kibanaQuerySettings) ); } else { return buildEsQuery( - derivedIndexPattern, + derivedDataView, coerceToQueries(query), filters ?? [], kibanaQuerySettings ); } - }, [derivedIndexPattern, filters, kibanaQuerySettings, query]); + }, [derivedDataView, filters, kibanaQuerySettings, query]); // Internal state const { @@ -158,8 +166,8 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac }); const columnConfigurations = useMemo(() => { - return sourceConfiguration ? customColumns ?? sourceConfiguration.configuration.logColumns : []; - }, [sourceConfiguration, customColumns]); + return resolvedLogView ? customColumns ?? resolvedLogView.columns : []; + }, [resolvedLogView, customColumns]); const streamItems = useMemo( () => @@ -173,8 +181,8 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac // Component lifetime useEffect(() => { - loadSource(); - }, [loadSource]); + loadLogView(); + }, [loadLogView]); useEffect(() => { fetchEntries(); @@ -207,7 +215,7 @@ Read more at https://github.com/elastic/kibana/blob/main/src/plugins/kibana_reac items={streamItems} scale="medium" wrap={true} - isReloading={isLoadingSource || isLoadingEntries} + isReloading={isLoadingLogView || isLoadingEntries} isLoadingMore={isLoadingMore} hasMoreBeforeStart={hasMoreBefore} hasMoreAfterEnd={hasMoreAfter} diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx index 39e58fb518b02..a834bc0af02a8 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx @@ -18,7 +18,7 @@ import { } from '../../../../../../src/plugins/embeddable/public'; import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { CoreProviders } from '../../apps/common_providers'; -import { InfraClientStartDeps } from '../../types'; +import { InfraClientStartDeps, InfraClientStartExports } from '../../types'; import { datemathToEpochMillis } from '../../utils/datemath'; import { LazyLogStreamWrapper } from './lazy_log_stream_wrapper'; @@ -38,6 +38,7 @@ export class LogStreamEmbeddable extends Embeddable { constructor( private core: CoreStart, private pluginDeps: InfraClientStartDeps, + private pluginStart: InfraClientStartExports, initialInput: LogStreamEmbeddableInput, parent?: IContainer ) { @@ -78,7 +79,12 @@ export class LogStreamEmbeddable extends Embeddable { } ReactDOM.render( - +
) {} + constructor(private getStartServices: InfraClientStartServicesAccessor) {} public async isEditable() { const [{ application }] = await this.getStartServices(); @@ -31,8 +30,8 @@ export class LogStreamEmbeddableFactoryDefinition } public async create(initialInput: LogStreamEmbeddableInput, parent?: IContainer) { - const [core, plugins] = await this.getStartServices(); - return new LogStreamEmbeddable(core, plugins, initialInput, parent); + const [core, plugins, pluginStart] = await this.getStartServices(); + return new LogStreamEmbeddable(core, plugins, pluginStart, initialInput, parent); } public getDisplayName() { diff --git a/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx index 23718bf16b401..ab4d763ab1f95 100644 --- a/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_source_error_page.tsx @@ -9,12 +9,12 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, EuiSpacer } from import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { SavedObjectNotFound } from '../../../../../../src/plugins/kibana_utils/common'; -import { - FetchLogSourceConfigurationError, - FetchLogSourceStatusError, - ResolveLogSourceConfigurationError, -} from '../../../common/log_sources'; import { useLinkProps } from '../../../../observability/public'; +import { + FetchLogViewStatusError, + FetchLogViewError, + ResolveLogViewError, +} from '../../../common/log_views'; import { LogsPageTemplate } from '../../pages/logs/page_template'; export const LogSourceErrorPage: React.FC<{ @@ -72,7 +72,7 @@ export const LogSourceErrorPage: React.FC<{ }; const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => { - if (error instanceof ResolveLogSourceConfigurationError) { + if (error instanceof ResolveLogViewError) { return ( = ({ error }) => { )} ); - } else if (error instanceof FetchLogSourceConfigurationError) { + } else if (error instanceof FetchLogViewError) { return ( = ({ error }) => { {`${error.cause?.message ?? error.message}`} ); - } else if (error instanceof FetchLogSourceStatusError) { + } else if (error instanceof FetchLogViewStatusError) { return ( { - const response = await fetch(getLogSourceConfigurationPath(sourceId), { - method: 'GET', - }).catch((error) => { - throw new FetchLogSourceConfigurationError( - `Failed to fetch log source configuration "${sourceId}": ${error}`, - error - ); - }); - - return decodeOrThrow( - getLogSourceConfigurationSuccessResponsePayloadRT, - (message: string) => - new FetchLogSourceConfigurationError( - `Failed to decode log source configuration "${sourceId}": ${message}` - ) - )(response); -}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts deleted file mode 100644 index 38e4378b88571..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts +++ /dev/null @@ -1,33 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpHandler } from 'src/core/public'; -import { - getLogSourceStatusPath, - getLogSourceStatusSuccessResponsePayloadRT, -} from '../../../../../common/http_api/log_sources'; -import { FetchLogSourceStatusError } from '../../../../../common/log_sources'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -export const callFetchLogSourceStatusAPI = async (sourceId: string, fetch: HttpHandler) => { - const response = await fetch(getLogSourceStatusPath(sourceId), { - method: 'GET', - }).catch((error) => { - throw new FetchLogSourceStatusError( - `Failed to fetch status for log source "${sourceId}": ${error}`, - error - ); - }); - - return decodeOrThrow( - getLogSourceStatusSuccessResponsePayloadRT, - (message: string) => - new FetchLogSourceStatusError( - `Failed to decode status for log source "${sourceId}": ${message}` - ) - )(response); -}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts deleted file mode 100644 index f469d2ab33421..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts +++ /dev/null @@ -1,44 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { HttpHandler } from 'src/core/public'; -import { - getLogSourceConfigurationPath, - patchLogSourceConfigurationSuccessResponsePayloadRT, - patchLogSourceConfigurationRequestBodyRT, - LogSourceConfigurationPropertiesPatch, -} from '../../../../../common/http_api/log_sources'; -import { PatchLogSourceConfigurationError } from '../../../../../common/log_sources'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -export const callPatchLogSourceConfigurationAPI = async ( - sourceId: string, - patchedProperties: LogSourceConfigurationPropertiesPatch, - fetch: HttpHandler -) => { - const response = await fetch(getLogSourceConfigurationPath(sourceId), { - method: 'PATCH', - body: JSON.stringify( - patchLogSourceConfigurationRequestBodyRT.encode({ - data: patchedProperties, - }) - ), - }).catch((error) => { - throw new PatchLogSourceConfigurationError( - `Failed to update log source configuration "${sourceId}": ${error}`, - error - ); - }); - - return decodeOrThrow( - patchLogSourceConfigurationSuccessResponsePayloadRT, - (message: string) => - new PatchLogSourceConfigurationError( - `Failed to decode log source configuration "${sourceId}": ${message}` - ) - )(response); -}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts deleted file mode 100644 index ad649ade7345a..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts +++ /dev/null @@ -1,85 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LogSourceConfiguration, LogSourceStatus, useLogSource } from './log_source'; - -type CreateUseLogSource = (sourceConfiguration?: { sourceId?: string }) => typeof useLogSource; - -const defaultSourceId = 'default'; - -export const createUninitializedUseLogSourceMock: CreateUseLogSource = - ({ sourceId = defaultSourceId } = {}) => - () => ({ - derivedIndexPattern: { - fields: [], - title: 'unknown', - }, - hasFailedLoading: false, - hasFailedLoadingSource: false, - hasFailedLoadingSourceStatus: false, - hasFailedResolvingSource: false, - initialize: jest.fn(), - isLoading: false, - isLoadingSourceConfiguration: false, - isLoadingSourceStatus: false, - isResolvingSourceConfiguration: false, - isUninitialized: true, - loadSource: jest.fn(), - loadSourceConfiguration: jest.fn(), - latestLoadSourceFailures: [], - resolveSourceFailureMessage: undefined, - loadSourceStatus: jest.fn(), - sourceConfiguration: undefined, - sourceId, - sourceStatus: undefined, - updateSource: jest.fn(), - resolvedSourceConfiguration: undefined, - loadResolveLogSourceConfiguration: jest.fn(), - }); - -export const createLoadingUseLogSourceMock: CreateUseLogSource = - ({ sourceId = defaultSourceId } = {}) => - (args) => ({ - ...createUninitializedUseLogSourceMock({ sourceId })(args), - isLoading: true, - isLoadingSourceConfiguration: true, - isLoadingSourceStatus: true, - isResolvingSourceConfiguration: true, - }); - -export const createLoadedUseLogSourceMock: CreateUseLogSource = - ({ sourceId = defaultSourceId } = {}) => - (args) => ({ - ...createUninitializedUseLogSourceMock({ sourceId })(args), - sourceConfiguration: createBasicSourceConfiguration(sourceId), - sourceStatus: { - indices: 'test-index', - logIndexStatus: 'available', - }, - }); - -export const createBasicSourceConfiguration = (sourceId: string): LogSourceConfiguration => ({ - id: sourceId, - origin: 'stored', - configuration: { - description: `description for ${sourceId}`, - logIndices: { - type: 'index_pattern', - indexPatternId: 'some-id', - }, - logColumns: [], - fields: { - message: ['MESSAGE_FIELD'], - }, - name: sourceId, - }, -}); - -export const createAvailableSourceStatus = (): LogSourceStatus => ({ - indices: 'test-index', - logIndexStatus: 'available', -}); diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts deleted file mode 100644 index 54f3f70b98a4b..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ /dev/null @@ -1,209 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import createContainer from 'constate'; -import { useCallback, useMemo, useState } from 'react'; -import type { HttpHandler } from 'src/core/public'; -import { DataViewsContract } from '../../../../../../../src/plugins/data_views/public'; -import { - LogIndexField, - LogSourceConfigurationPropertiesPatch, - LogSourceStatus, -} from '../../../../common/http_api/log_sources'; -import { - LogSourceConfiguration, - LogSourceConfigurationProperties, - ResolvedLogSourceConfiguration, - resolveLogSourceConfiguration, - ResolveLogSourceConfigurationError, -} from '../../../../common/log_sources'; -import { isRejectedPromiseState, useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callFetchLogSourceConfigurationAPI } from './api/fetch_log_source_configuration'; -import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status'; -import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration'; - -export type { - LogIndexField, - LogSourceConfiguration, - LogSourceConfigurationProperties, - LogSourceConfigurationPropertiesPatch, - LogSourceStatus, -}; -export { ResolveLogSourceConfigurationError }; - -export const useLogSource = ({ - sourceId, - fetch, - indexPatternsService, -}: { - sourceId: string; - fetch: HttpHandler; - indexPatternsService: DataViewsContract; -}) => { - const [sourceConfiguration, setSourceConfiguration] = useState< - LogSourceConfiguration | undefined - >(undefined); - - const [resolvedSourceConfiguration, setResolvedSourceConfiguration] = useState< - ResolvedLogSourceConfiguration | undefined - >(undefined); - - const [sourceStatus, setSourceStatus] = useState(undefined); - - const [loadSourceConfigurationRequest, loadSourceConfiguration] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - return (await callFetchLogSourceConfigurationAPI(sourceId, fetch)).data; - }, - onResolve: setSourceConfiguration, - }, - [sourceId, fetch, indexPatternsService] - ); - - const [resolveSourceConfigurationRequest, resolveSourceConfiguration] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async (unresolvedSourceConfiguration: LogSourceConfigurationProperties) => { - return await resolveLogSourceConfiguration( - unresolvedSourceConfiguration, - indexPatternsService - ); - }, - onResolve: setResolvedSourceConfiguration, - }, - [indexPatternsService] - ); - - const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => { - return (await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties, fetch)).data; - }, - onResolve: setSourceConfiguration, - }, - [sourceId, fetch, indexPatternsService] - ); - - const [loadSourceStatusRequest, loadSourceStatus] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - return await callFetchLogSourceStatusAPI(sourceId, fetch); - }, - onResolve: ({ data }) => setSourceStatus(data), - }, - [sourceId, fetch] - ); - - const derivedIndexPattern = useMemo( - () => ({ - fields: resolvedSourceConfiguration?.fields ?? [], - title: resolvedSourceConfiguration?.indices ?? 'unknown', - }), - [resolvedSourceConfiguration] - ); - - const isLoadingSourceConfiguration = loadSourceConfigurationRequest.state === 'pending'; - const isResolvingSourceConfiguration = resolveSourceConfigurationRequest.state === 'pending'; - const isLoadingSourceStatus = loadSourceStatusRequest.state === 'pending'; - const isUpdatingSourceConfiguration = updateSourceConfigurationRequest.state === 'pending'; - - const isLoading = - isLoadingSourceConfiguration || - isResolvingSourceConfiguration || - isLoadingSourceStatus || - isUpdatingSourceConfiguration; - - const isUninitialized = - loadSourceConfigurationRequest.state === 'uninitialized' || - resolveSourceConfigurationRequest.state === 'uninitialized' || - loadSourceStatusRequest.state === 'uninitialized'; - - const hasFailedLoadingSource = loadSourceConfigurationRequest.state === 'rejected'; - const hasFailedResolvingSource = resolveSourceConfigurationRequest.state === 'rejected'; - const hasFailedLoadingSourceStatus = loadSourceStatusRequest.state === 'rejected'; - - const latestLoadSourceFailures = [ - loadSourceConfigurationRequest, - resolveSourceConfigurationRequest, - loadSourceStatusRequest, - ] - .filter(isRejectedPromiseState) - .map(({ value }) => (value instanceof Error ? value : new Error(`${value}`))); - - const hasFailedLoading = latestLoadSourceFailures.length > 0; - - const loadSource = useCallback(async () => { - const loadSourceConfigurationPromise = loadSourceConfiguration(); - const loadSourceStatusPromise = loadSourceStatus(); - const resolveSourceConfigurationPromise = resolveSourceConfiguration( - (await loadSourceConfigurationPromise).configuration - ); - - return await Promise.all([ - loadSourceConfigurationPromise, - resolveSourceConfigurationPromise, - loadSourceStatusPromise, - ]); - }, [loadSourceConfiguration, loadSourceStatus, resolveSourceConfiguration]); - - const updateSource = useCallback( - async (patchedProperties: LogSourceConfigurationPropertiesPatch) => { - const updatedSourceConfiguration = await updateSourceConfiguration(patchedProperties); - const resolveSourceConfigurationPromise = resolveSourceConfiguration( - updatedSourceConfiguration.configuration - ); - const loadSourceStatusPromise = loadSourceStatus(); - - return await Promise.all([ - updatedSourceConfiguration, - resolveSourceConfigurationPromise, - loadSourceStatusPromise, - ]); - }, - [loadSourceStatus, resolveSourceConfiguration, updateSourceConfiguration] - ); - - const initialize = useCallback(async () => { - if (!isUninitialized) { - return; - } - - return await loadSource(); - }, [isUninitialized, loadSource]); - - return { - sourceId, - initialize, - isUninitialized, - derivedIndexPattern, - // Failure states - hasFailedLoading, - hasFailedLoadingSource, - hasFailedLoadingSourceStatus, - hasFailedResolvingSource, - latestLoadSourceFailures, - // Loading states - isLoading, - isLoadingSourceConfiguration, - isLoadingSourceStatus, - isResolvingSourceConfiguration, - // Source status (denotes the state of the indices, e.g. missing) - sourceStatus, - loadSourceStatus, - // Source configuration (represents the raw attributes of the source configuration) - loadSource, - sourceConfiguration, - updateSource, - // Resolved source configuration (represents a fully resolved state, you would use this for the vast majority of "read" scenarios) - resolvedSourceConfiguration, - }; -}; - -export const [LogSourceProvider, useLogSourceContext] = createContainer(useLogSource); 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 dc9ab56aa9e86..2a81afa234729 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 @@ -12,8 +12,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import useSetState from 'react-use/lib/useSetState'; import { LogEntry, LogEntryCursor } from '../../../../common/log_entry'; +import { LogViewColumnConfiguration } from '../../../../common/log_views'; import { useSubscription } from '../../../utils/use_observable'; -import { LogSourceConfigurationProperties } from '../log_source'; import { useFetchLogEntriesAfter } from './use_fetch_log_entries_after'; import { useFetchLogEntriesAround } from './use_fetch_log_entries_around'; import { useFetchLogEntriesBefore } from './use_fetch_log_entries_before'; @@ -26,7 +26,7 @@ interface LogStreamProps { endTimestamp: number; query?: BuiltEsQuery; center?: LogEntryCursor; - columns?: LogSourceConfigurationProperties['logColumns']; + columns?: LogViewColumnConfiguration[]; } interface LogStreamState { diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_after.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_after.ts index ea2162cb96e36..b4bdd72b3c5ee 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_after.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_after.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { JsonObject } from '@kbn/utility-types'; import { useCallback } from 'react'; import { Observable } from 'rxjs'; import { exhaustMap } from 'rxjs/operators'; -import { JsonObject } from '@kbn/utility-types'; import { IKibanaSearchRequest } from '../../../../../../../src/plugins/data/public'; -import { LogSourceColumnConfiguration } from '../../../../common/log_sources'; import { LogEntryAfterCursor } from '../../../../common/log_entry'; +import { LogViewColumnConfiguration } from '../../../../common/log_views'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { logEntriesSearchRequestParamsRT, @@ -37,7 +37,7 @@ export const useLogEntriesAfterRequest = ({ sourceId, startTimestamp, }: { - columnOverrides?: LogSourceColumnConfiguration[]; + columnOverrides?: LogViewColumnConfiguration[]; endTimestamp: number; highlightPhrase?: string; query?: LogEntriesSearchRequestQuery; @@ -110,7 +110,7 @@ export const useFetchLogEntriesAfter = ({ sourceId, startTimestamp, }: { - columnOverrides?: LogSourceColumnConfiguration[]; + columnOverrides?: LogViewColumnConfiguration[]; endTimestamp: number; highlightPhrase?: string; query?: LogEntriesSearchRequestQuery; diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_around.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_around.ts index d2b014c22ad1b..748281d5517a7 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_around.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_around.ts @@ -8,8 +8,8 @@ import { useCallback } from 'react'; import { combineLatest, Observable, ReplaySubject } from 'rxjs'; import { last, map, startWith, switchMap } from 'rxjs/operators'; -import { LogSourceColumnConfiguration } from '../../../../common/log_sources'; import { LogEntryCursor } from '../../../../common/log_entry'; +import { LogViewColumnConfiguration } from '../../../../common/log_views'; import { LogEntriesSearchRequestQuery } from '../../../../common/search_strategies/log_entries/log_entries'; import { flattenDataSearchResponseDescriptor } from '../../../utils/data_search'; import { useObservable, useObservableState } from '../../../utils/use_observable'; @@ -24,7 +24,7 @@ export const useFetchLogEntriesAround = ({ sourceId, startTimestamp, }: { - columnOverrides?: LogSourceColumnConfiguration[]; + columnOverrides?: LogViewColumnConfiguration[]; endTimestamp: number; highlightPhrase?: string; query?: LogEntriesSearchRequestQuery; diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_before.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_before.ts index 7d99b3069d973..b9acb0bb38692 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_before.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/use_fetch_log_entries_before.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { JsonObject } from '@kbn/utility-types'; import { useCallback } from 'react'; import { Observable } from 'rxjs'; import { exhaustMap } from 'rxjs/operators'; -import { JsonObject } from '@kbn/utility-types'; import { IKibanaSearchRequest } from '../../../../../../../src/plugins/data/public'; -import { LogSourceColumnConfiguration } from '../../../../common/log_sources'; import { LogEntryBeforeCursor } from '../../../../common/log_entry'; +import { LogViewColumnConfiguration } from '../../../../common/log_views'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { logEntriesSearchRequestParamsRT, @@ -37,7 +37,7 @@ export const useLogEntriesBeforeRequest = ({ sourceId, startTimestamp, }: { - columnOverrides?: LogSourceColumnConfiguration[]; + columnOverrides?: LogViewColumnConfiguration[]; endTimestamp: number; highlightPhrase?: string; query?: LogEntriesSearchRequestQuery; @@ -109,7 +109,7 @@ export const useFetchLogEntriesBefore = ({ sourceId, startTimestamp, }: { - columnOverrides?: LogSourceColumnConfiguration[]; + columnOverrides?: LogViewColumnConfiguration[]; endTimestamp: number; highlightPhrase?: string; query?: LogEntriesSearchRequestQuery; diff --git a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts index 9204c81816e83..1b6d9f850187a 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts @@ -7,10 +7,10 @@ import { useContext } from 'react'; import useThrottle from 'react-use/lib/useThrottle'; +import { useLogViewContext } from '../../../hooks/use_log_view'; import { RendererFunction } from '../../../utils/typed_react'; import { LogFilterState } from '../log_filter'; import { LogPositionState } from '../log_position'; -import { useLogSourceContext } from '../log_source'; import { LogSummaryBuckets, useLogSummary } from './log_summary'; const FETCH_THROTTLE_INTERVAL = 3000; @@ -24,7 +24,7 @@ export const WithSummary = ({ end: number | null; }>; }) => { - const { sourceId } = useLogSourceContext(); + const { logViewId } = useLogViewContext(); const { filterQuery } = useContext(LogFilterState.Context); const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context); @@ -33,7 +33,7 @@ export const WithSummary = ({ const throttledEndTimestamp = useThrottle(endTimestamp, FETCH_THROTTLE_INTERVAL); const { buckets, start, end } = useLogSummary( - sourceId, + logViewId, throttledStartTimestamp, throttledEndTimestamp, filterQuery?.serializedQuery ?? null diff --git a/x-pack/plugins/infra/public/hooks/use_kibana.ts b/x-pack/plugins/infra/public/hooks/use_kibana.ts deleted file mode 100644 index 1d21f352a9ea4..0000000000000 --- a/x-pack/plugins/infra/public/hooks/use_kibana.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreStart } from '../../../../../src/core/public'; -import { - createKibanaReactContext, - KibanaReactContextValue, - useKibana, -} from '../../../../../src/plugins/kibana_react/public'; -import { InfraClientStartDeps } from '../types'; - -export type PluginKibanaContextValue = CoreStart & InfraClientStartDeps; - -export const createKibanaContextForPlugin = (core: CoreStart, pluginsStart: InfraClientStartDeps) => - createKibanaReactContext({ - ...core, - ...pluginsStart, - }); - -export const useKibanaContextForPlugin = - useKibana as () => KibanaReactContextValue; diff --git a/x-pack/plugins/infra/public/hooks/use_kibana.tsx b/x-pack/plugins/infra/public/hooks/use_kibana.tsx new file mode 100644 index 0000000000000..e7f9295a09bae --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_kibana.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PropsOf } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { CoreStart } from '../../../../../src/core/public'; +import { + createKibanaReactContext, + KibanaReactContextValue, + useKibana, +} from '../../../../../src/plugins/kibana_react/public'; +import { InfraClientCoreSetup, InfraClientStartDeps, InfraClientStartExports } from '../types'; + +export type PluginKibanaContextValue = CoreStart & InfraClientStartDeps & InfraClientStartExports; + +export const createKibanaContextForPlugin = ( + core: CoreStart, + plugins: InfraClientStartDeps, + pluginStart: InfraClientStartExports +) => + createKibanaReactContext({ + ...core, + ...plugins, + ...pluginStart, + }); + +export const useKibanaContextForPlugin = + useKibana as () => KibanaReactContextValue; + +export const useKibanaContextForPluginProvider = ( + core: CoreStart, + plugins: InfraClientStartDeps, + pluginStart: InfraClientStartExports +) => { + const { Provider } = useMemo( + () => createKibanaContextForPlugin(core, plugins, pluginStart), + [core, pluginStart, plugins] + ); + + return Provider; +}; + +export const createLazyComponentWithKibanaContext = >( + coreSetup: InfraClientCoreSetup, + lazyComponentFactory: () => Promise<{ default: T }> +) => + React.lazy(() => + Promise.all([lazyComponentFactory(), coreSetup.getStartServices()]).then( + ([{ default: LazilyLoadedComponent }, [core, plugins, pluginStart]]) => { + const { Provider } = createKibanaContextForPlugin(core, plugins, pluginStart); + + return { + default: (props: PropsOf) => ( + + + + ), + }; + } + ) + ); diff --git a/x-pack/plugins/infra/public/hooks/use_log_view.mock.ts b/x-pack/plugins/infra/public/hooks/use_log_view.mock.ts new file mode 100644 index 0000000000000..daebfb82b4564 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_log_view.mock.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createLogViewMock } from '../../common/log_views/log_view.mock'; +import { createResolvedLogViewMockFromAttributes } from '../../common/log_views/resolved_log_view.mock'; +import { useLogView } from './use_log_view'; + +type UseLogView = typeof useLogView; +type IUseLogView = ReturnType; + +const defaultLogViewId = 'default'; + +export const createUninitializedUseLogViewMock = + (logViewId: string = defaultLogViewId) => + (): IUseLogView => ({ + derivedDataView: { + fields: [], + title: 'unknown', + }, + hasFailedLoading: false, + hasFailedLoadingLogView: false, + hasFailedLoadingLogViewStatus: false, + hasFailedResolvingLogView: false, + isLoading: false, + isLoadingLogView: false, + isLoadingLogViewStatus: false, + isResolvingLogView: false, + isUninitialized: true, + latestLoadLogViewFailures: [], + load: jest.fn(), + logView: undefined, + logViewId, + logViewStatus: undefined, + resolvedLogView: undefined, + update: jest.fn(), + }); + +export const createLoadingUseLogViewMock = + (logViewId: string = defaultLogViewId) => + (): IUseLogView => ({ + ...createUninitializedUseLogViewMock(logViewId)(), + isLoading: true, + isLoadingLogView: true, + isLoadingLogViewStatus: true, + isResolvingLogView: true, + }); + +export const createLoadedUseLogViewMock = async (logViewId: string = defaultLogViewId) => { + const logView = createLogViewMock(logViewId); + const resolvedLogView = await createResolvedLogViewMockFromAttributes(logView.attributes); + + return (): IUseLogView => { + return { + ...createUninitializedUseLogViewMock(logViewId)(), + logView, + resolvedLogView, + logViewStatus: { + index: 'available', + }, + }; + }; +}; diff --git a/x-pack/plugins/infra/public/hooks/use_log_view.ts b/x-pack/plugins/infra/public/hooks/use_log_view.ts new file mode 100644 index 0000000000000..95363d12a9135 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_log_view.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import createContainer from 'constate'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { HttpHandler } from 'src/core/public'; +import { LogView, LogViewAttributes, LogViewStatus, ResolvedLogView } from '../../common/log_views'; +import type { ILogViewsClient } from '../services/log_views'; +import { isRejectedPromiseState, useTrackedPromise } from '../utils/use_tracked_promise'; + +export const useLogView = ({ + logViewId, + logViews, + fetch, +}: { + logViewId: string; + logViews: ILogViewsClient; + fetch: HttpHandler; +}) => { + const [logView, setLogView] = useState(undefined); + + const [resolvedLogView, setResolvedLogView] = useState(undefined); + + const [logViewStatus, setLogViewStatus] = useState(undefined); + + const [loadLogViewRequest, loadLogView] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: logViews.getLogView.bind(logViews), + onResolve: setLogView, + }, + [logViews] + ); + + const [resolveLogViewRequest, resolveLogView] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: logViews.resolveLogView.bind(logViews), + onResolve: setResolvedLogView, + }, + [logViews] + ); + + const [updateLogViewRequest, updateLogView] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: logViews.putLogView.bind(logViews), + onResolve: setLogView, + }, + [logViews] + ); + + const [loadLogViewStatusRequest, loadLogViewStatus] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: logViews.getResolvedLogViewStatus.bind(logViews), + onResolve: setLogViewStatus, + }, + [logViews] + ); + + const derivedDataView = useMemo( + () => ({ + fields: resolvedLogView?.fields ?? [], + title: resolvedLogView?.indices ?? 'unknown', + }), + [resolvedLogView] + ); + + const isLoadingLogView = loadLogViewRequest.state === 'pending'; + const isResolvingLogView = resolveLogViewRequest.state === 'pending'; + const isLoadingLogViewStatus = loadLogViewStatusRequest.state === 'pending'; + const isUpdatingLogView = updateLogViewRequest.state === 'pending'; + + const isLoading = + isLoadingLogView || isResolvingLogView || isLoadingLogViewStatus || isUpdatingLogView; + + const isUninitialized = loadLogViewRequest.state === 'uninitialized'; + + const hasFailedLoadingLogView = loadLogViewRequest.state === 'rejected'; + const hasFailedResolvingLogView = resolveLogViewRequest.state === 'rejected'; + const hasFailedLoadingLogViewStatus = loadLogViewStatusRequest.state === 'rejected'; + + const latestLoadLogViewFailures = [ + loadLogViewRequest, + resolveLogViewRequest, + loadLogViewStatusRequest, + ] + .filter(isRejectedPromiseState) + .map(({ value }) => (value instanceof Error ? value : new Error(`${value}`))); + + const hasFailedLoading = latestLoadLogViewFailures.length > 0; + + const load = useCallback(async () => { + const loadedLogView = await loadLogView(logViewId); + const resolvedLoadedLogView = await resolveLogView(loadedLogView.attributes); + const resolvedLogViewStatus = await loadLogViewStatus(resolvedLoadedLogView); + + return [loadedLogView, resolvedLoadedLogView, resolvedLogViewStatus]; + }, [logViewId, loadLogView, loadLogViewStatus, resolveLogView]); + + const update = useCallback( + async (logViewAttributes: Partial) => { + const updatedLogView = await updateLogView(logViewId, logViewAttributes); + const resolvedUpdatedLogView = await resolveLogView(updatedLogView.attributes); + const resolvedLogViewStatus = await loadLogViewStatus(resolvedUpdatedLogView); + + return [updatedLogView, resolvedUpdatedLogView, resolvedLogViewStatus]; + }, + [logViewId, loadLogViewStatus, resolveLogView, updateLogView] + ); + + useEffect(() => { + load(); + }, [load]); + + return { + logViewId, + isUninitialized, + derivedDataView, + + // Failure states + hasFailedLoading, + hasFailedLoadingLogView, + hasFailedLoadingLogViewStatus, + hasFailedResolvingLogView, + latestLoadLogViewFailures, + + // Loading states + isLoading, + isLoadingLogView, + isLoadingLogViewStatus, + isResolvingLogView, + + // data + logView, + resolvedLogView, + logViewStatus, + + // actions + load, + update, + }; +}; + +export const [LogViewProvider, useLogViewContext] = createContainer(useLogView); diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts index 806947b1e5c3f..2f09fde24cada 100644 --- a/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts @@ -5,21 +5,24 @@ * 2.0. */ -import { coreMock } from 'src/core/public/mocks'; -import { createMetricsHasData, createMetricsFetchData } from './metrics_overview_fetchers'; import { CoreStart } from 'kibana/public'; -import { InfraClientStartDeps, InfraClientStartExports } from './types'; import moment from 'moment'; +import { coreMock } from 'src/core/public/mocks'; +import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers'; +import { createInfraPluginStartMock } from './mocks'; import { FAKE_OVERVIEW_RESPONSE } from './test_utils'; +import { InfraClientStartDeps, InfraClientStartExports } from './types'; function setup() { const core = coreMock.createStart(); + const pluginStart = createInfraPluginStartMock(); + const mockedGetStartServices = jest.fn(() => { const deps = {}; return Promise.resolve([ core as CoreStart, deps as InfraClientStartDeps, - {} as InfraClientStartExports, + pluginStart, ]) as Promise<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>; }); return { core, mockedGetStartServices }; diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts index 1f4130368578d..831d05fee58a2 100644 --- a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts @@ -15,11 +15,11 @@ import { FetchDataParams, MetricsFetchDataResponse } from '../../observability/public'; import { TopNodesRequest, TopNodesResponse } from '../common/http_api/overview_api'; -import { InfraClientCoreSetup } from './types'; import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; +import { InfraClientStartServicesAccessor } from './types'; export const createMetricsHasData = - (getStartServices: InfraClientCoreSetup['getStartServices']) => async () => { + (getStartServices: InfraClientStartServicesAccessor) => async () => { const [coreServices] = await getStartServices(); const { http } = coreServices; const results = await http.get<{ @@ -30,7 +30,7 @@ export const createMetricsHasData = }; export const createMetricsFetchData = - (getStartServices: InfraClientCoreSetup['getStartServices']) => + (getStartServices: InfraClientStartServicesAccessor) => async ({ absoluteTime, intervalString }: FetchDataParams): Promise => { const [coreServices] = await getStartServices(); const { http } = coreServices; diff --git a/x-pack/plugins/infra/public/mocks.tsx b/x-pack/plugins/infra/public/mocks.tsx new file mode 100644 index 0000000000000..1f6496ab569ab --- /dev/null +++ b/x-pack/plugins/infra/public/mocks.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock'; +import { InfraClientStartExports } from './types'; + +export const createInfraPluginStartMock = () => ({ + logViews: createLogViewsServiceStartMock(), + ContainerMetricsTable: () =>
, + HostMetricsTable: () =>
, + PodMetricsTable: () =>
, +}); + +export const _ensureTypeCompatibility = (): InfraClientStartExports => createInfraPluginStartMock(); diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx index cfcf8db771b78..ae564622c1ba2 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx @@ -11,22 +11,22 @@ import React from 'react'; import { Route, Router, Switch } from 'react-router-dom'; import { httpServiceMock } from 'src/core/public/mocks'; import { KibanaContextProvider, KibanaPageTemplate } from 'src/plugins/kibana_react/public'; -import { useLogSource } from '../../containers/logs/log_source'; +import { useLogView } from '../../hooks/use_log_view'; import { - createLoadedUseLogSourceMock, - createLoadingUseLogSourceMock, -} from '../../containers/logs/log_source/log_source.mock'; + createLoadedUseLogViewMock, + createLoadingUseLogViewMock, +} from '../../hooks/use_log_view.mock'; import { LinkToLogsPage } from './link_to_logs'; -jest.mock('../../containers/logs/log_source'); -const useLogSourceMock = useLogSource as jest.MockedFunction; +jest.mock('../../hooks/use_log_view'); +const useLogViewMock = useLogView as jest.MockedFunction; const renderRoutes = (routes: React.ReactElement) => { const history = createMemoryHistory(); const services = { http: httpServiceMock.createStartContract(), - data: { - indexPatterns: {}, + logViews: { + client: {}, }, observability: { navigation: { @@ -48,12 +48,12 @@ const renderRoutes = (routes: React.ReactElement) => { }; describe('LinkToLogsPage component', () => { - beforeEach(() => { - useLogSourceMock.mockImplementation(createLoadedUseLogSourceMock()); + beforeEach(async () => { + useLogViewMock.mockImplementation(await createLoadedUseLogViewMock()); }); afterEach(() => { - useLogSourceMock.mockRestore(); + useLogViewMock.mockRestore(); }); describe('default route', () => { @@ -199,7 +199,7 @@ describe('LinkToLogsPage component', () => { }); it('renders a loading page while loading the source configuration', async () => { - useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock()); + useLogViewMock.mockImplementation(createLoadingUseLogViewMock()); const { history, queryByTestId } = renderRoutes( @@ -209,7 +209,7 @@ describe('LinkToLogsPage component', () => { history.push('/link-to/host-logs/HOST_NAME'); await waitFor(() => { - expect(queryByTestId('nodeLoadingPage-host')).not.toBeEmpty(); + expect(queryByTestId('nodeLoadingPage-host')).not.toBeEmptyDOMElement(); }); }); }); @@ -258,7 +258,7 @@ describe('LinkToLogsPage component', () => { }); it('renders a loading page while loading the source configuration', () => { - useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock()); + useLogViewMock.mockImplementation(createLoadingUseLogViewMock()); const { history, queryByTestId } = renderRoutes( @@ -268,7 +268,7 @@ describe('LinkToLogsPage component', () => { history.push('/link-to/container-logs/CONTAINER_ID'); - expect(queryByTestId('nodeLoadingPage-container')).not.toBeEmpty(); + expect(queryByTestId('nodeLoadingPage-container')).not.toBeEmptyDOMElement(); }); }); @@ -314,7 +314,7 @@ describe('LinkToLogsPage component', () => { }); it('renders a loading page while loading the source configuration', () => { - useLogSourceMock.mockImplementation(createLoadingUseLogSourceMock()); + useLogViewMock.mockImplementation(createLoadingUseLogViewMock()); const { history, queryByTestId } = renderRoutes( @@ -324,7 +324,7 @@ describe('LinkToLogsPage component', () => { history.push('/link-to/pod-logs/POD_UID'); - expect(queryByTestId('nodeLoadingPage-pod')).not.toBeEmpty(); + expect(queryByTestId('nodeLoadingPage-pod')).not.toBeEmptyDOMElement(); }); }); }); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index 8aaac2f1b9a46..c8bd111402d46 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -6,20 +6,20 @@ */ import { i18n } from '@kbn/i18n'; +import { flowRight } from 'lodash'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import useMount from 'react-use/lib/useMount'; -import { flowRight } from 'lodash'; +import { LinkDescriptor } from '../../../../observability/public'; import { findInventoryFields } from '../../../common/inventory_models'; import { InventoryItemType } from '../../../common/inventory_models/types'; import { LoadingPage } from '../../components/loading_page'; import { replaceLogFilterInQueryString } from '../../containers/logs/log_filter'; import { replaceLogPositionInQueryString } from '../../containers/logs/log_position'; -import { useLogSource } from '../../containers/logs/log_source'; import { replaceSourceIdInQueryString } from '../../containers/source_id'; -import { LinkDescriptor } from '../../../../observability/public'; -import { getFilterFromLocation, getTimeFromLocation } from './query_params'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; +import { useLogView } from '../../hooks/use_log_view'; +import { getFilterFromLocation, getTimeFromLocation } from './query_params'; type RedirectToNodeLogsType = RouteComponentProps<{ nodeId: string; @@ -34,14 +34,14 @@ export const RedirectToNodeLogs = ({ location, }: RedirectToNodeLogsType) => { const { services } = useKibanaContextForPlugin(); - const { isLoading, loadSource } = useLogSource({ + const { isLoading, load } = useLogView({ fetch: services.http.fetch, - sourceId, - indexPatternsService: services.data.indexPatterns, + logViewId: sourceId, + logViews: services.logViews.client, }); useMount(() => { - loadSource(); + load(); }); if (isLoading) { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index 542b1dcd21d80..252d4ff737c15 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useEffect } from 'react'; +import type { LazyObservabilityPageTemplateProps } from '../../../../../observability/public'; import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { @@ -21,11 +22,10 @@ import { import { SubscriptionSplashPage } from '../../../components/subscription_splash_content'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; +import { useLogViewContext } from '../../../hooks/use_log_view'; +import { LogsPageTemplate } from '../page_template'; import { LogEntryCategoriesResultsContent } from './page_results_content'; import { LogEntryCategoriesSetupContent } from './page_setup_content'; -import { LogsPageTemplate } from '../page_template'; -import type { LazyObservabilityPageTemplateProps } from '../../../../../observability/public'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; const logCategoriesTitle = i18n.translate('xpack.infra.logs.logCategoriesTitle', { defaultMessage: 'Categories', @@ -115,10 +115,10 @@ const CategoriesPageTemplate: React.FC = ({ children, ...rest }) => { - const { sourceStatus } = useLogSourceContext(); + const { logViewStatus } = useLogViewContext(); return ( { const { hasFailedLoading, isLoading, isUninitialized, - latestLoadSourceFailures, - loadSource, - resolvedSourceConfiguration, - sourceId, - } = useLogSourceContext(); + latestLoadLogViewFailures, + load, + resolvedLogView, + logViewId, + } = useLogViewContext(); const { space } = useActiveKibanaSpace(); // This is a rather crude way of guarding the dependent providers against @@ -31,17 +31,17 @@ export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ child if (space == null) { return null; } else if (hasFailedLoading) { - return ; + return ; } else if (isLoading || isUninitialized) { return ; - } else if (resolvedSourceConfiguration != null) { + } else if (resolvedLogView != null) { return ( {children} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index 190050b504ee3..bba95b0fffb05 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -11,13 +11,21 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import useInterval from 'react-use/lib/useInterval'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { MLJobsAwaitingNodeWarning, ML_PAGES, useMlHref } from '../../../../../ml/public'; import { useTrackPageview } from '../../../../../observability/public'; import { TimeRange } from '../../../../common/time/time_range'; import { CategoryJobNoticesSection } from '../../../components/logging/log_analysis_job_status'; +import { AnalyzeInMlButton } from '../../../components/logging/log_analysis_results'; +import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector'; +import { RecreateJobButton } from '../../../components/logging/log_analysis_setup/create_job_button'; +import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { useLogViewContext } from '../../../hooks/use_log_view'; +import { LogsPageTemplate } from '../page_template'; import { PageViewLogInContext } from '../stream/page_view_log_in_context'; import { TopCategoriesSection } from './sections/top_categories'; import { useLogEntryCategoriesResults } from './use_log_entry_categories_results'; @@ -25,15 +33,6 @@ import { StringTimeRange, useLogEntryCategoriesResultsUrlState, } from './use_log_entry_categories_results_url_state'; -import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { LogsPageTemplate } from '../page_template'; -import { RecreateJobButton } from '../../../components/logging/log_analysis_setup/create_job_button'; -import { AnalyzeInMlButton } from '../../../components/logging/log_analysis_results'; -import { useMlHref, ML_PAGES } from '../../../../../ml/public'; -import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; -import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public'; const JOB_STATUS_POLLING_INTERVAL = 30000; @@ -52,7 +51,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent< services: { ml, http }, } = useKibanaContextForPlugin(); - const { sourceStatus } = useLogSourceContext(); + const { logViewStatus } = useLogViewContext(); const { hasLogAnalysisSetupCapabilities } = useLogAnalysisCapabilitiesContext(); const { @@ -212,7 +211,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent< endTimestamp={categoryQueryTimeRange.timeRange.endTime} > = ({ children, ...rest }) => { - const { sourceStatus } = useLogSourceContext(); + const { logViewStatus } = useLogViewContext(); return ( { const { hasFailedLoading, isLoading, isUninitialized, - latestLoadSourceFailures, - loadSource, - resolvedSourceConfiguration, - sourceId, - } = useLogSourceContext(); + latestLoadLogViewFailures, + load, + logViewId, + resolvedLogView, + } = useLogViewContext(); const { space } = useActiveKibanaSpace(); // This is a rather crude way of guarding the dependent providers against @@ -35,23 +35,23 @@ export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) } else if (isLoading || isUninitialized) { return ; } else if (hasFailedLoading) { - return ; - } else if (resolvedSourceConfiguration != null) { + return ; + } else if (resolvedLogView != null) { return ( {children} 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 a68432472c245..cf7a4789a687d 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 @@ -6,34 +6,34 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker } from '@elastic/eui'; +import type { Query } from '@kbn/es-query'; import moment from 'moment'; import { stringify } from 'query-string'; import React, { useCallback, useMemo } from 'react'; import { encode, RisonValue } from 'rison-node'; -import type { Query } from '@kbn/es-query'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public'; import { useTrackPageview } from '../../../../../observability/public'; +import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { TimeKey } from '../../../../common/time'; import { CategoryJobNoticesSection, LogAnalysisJobProblemIndicator, } from '../../../components/logging/log_analysis_job_status'; import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector'; +import { ManageJobsButton } from '../../../components/logging/log_analysis_setup/manage_jobs_button'; import { useLogAnalysisSetupFlyoutStateContext } from '../../../components/logging/log_analysis_setup/setup_flyout'; import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; import { useLogEntryFlyoutContext } from '../../../containers/logs/log_flyout'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; +import { useLogViewContext } from '../../../hooks/use_log_view'; +import { LogsPageTemplate } from '../page_template'; import { AnomaliesResults } from './sections/anomalies'; import { useDatasetFiltering } from './use_dataset_filtering'; import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results'; import { useLogAnalysisResultsUrlState } from './use_log_entry_rate_results_url_state'; -import { isJobStatusWithResults } from '../../../../common/log_analysis'; -import { LogsPageTemplate } from '../page_template'; -import { ManageJobsButton } from '../../../components/logging/log_analysis_setup/manage_jobs_button'; -import { MLJobsAwaitingNodeWarning } from '../../../../../ml/public'; export const SORT_DEFAULTS = { direction: 'desc' as const, @@ -52,7 +52,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{ const navigateToApp = useKibana().services.application?.navigateToApp; - const { sourceId, sourceStatus } = useLogSourceContext(); + const { logViewId, logViewStatus } = useLogViewContext(); const { hasLogAnalysisSetupCapabilities } = useLogAnalysisCapabilitiesContext(); @@ -142,7 +142,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{ datasets, isLoadingDatasets, } = useLogEntryAnomaliesResults({ - sourceId, + sourceId: logViewId, startTime: timeRange.value.startTime, endTime: timeRange.value.endTime, defaultSortOptions: SORT_DEFAULTS, @@ -196,7 +196,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{ return ( ], @@ -272,7 +272,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent<{ logEntryId={flyoutLogEntryId} onCloseFlyout={closeLogEntryFlyout} onSetFieldFilter={linkToLogStream} - sourceId={sourceId} + sourceId={logViewId} /> ) : null} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx index a5eb5e2219460..85f96884fbae5 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx @@ -11,10 +11,10 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import useMount from 'react-use/lib/useMount'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { LogEntryAnomaly, isCategoryAnomaly } from '../../../../../../common/log_analysis'; +import { isCategoryAnomaly, LogEntryAnomaly } from '../../../../../../common/log_analysis'; import { TimeRange } from '../../../../../../common/time/time_range'; import { LogEntryExampleMessages } from '../../../../../components/logging/log_entry_examples/log_entry_examples'; -import { useLogSourceContext } from '../../../../../containers/logs/log_source'; +import { useLogViewContext } from '../../../../../hooks/use_log_view'; import { useLogEntryExamples } from '../../use_log_entry_examples'; import { LogEntryExampleMessage, LogEntryExampleMessageHeaders } from './log_entry_example'; @@ -28,7 +28,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ anomaly: LogEntryAnomaly; timeRange: TimeRange; }> = ({ anomaly, timeRange }) => { - const { sourceId } = useLogSourceContext(); + const { logViewId } = useLogViewContext(); const { getLogEntryExamples, @@ -39,7 +39,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ dataset: anomaly.dataset, endTime: anomaly.startTime + anomaly.duration, exampleCount: EXAMPLE_COUNT, - sourceId, + sourceId: logViewId, startTime: anomaly.startTime, categoryId: isCategoryAnomaly(anomaly) ? anomaly.categoryId : undefined, }); 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 9643483fd199b..785a1d5f691da 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -5,41 +5,31 @@ * 2.0. */ -import { EuiHeaderLinks, EuiHeaderLink } from '@elastic/eui'; +import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { Route, Switch } from 'react-router-dom'; -import useMount from 'react-use/lib/useMount'; - -import { AlertDropdown } from '../../alerting/log_threshold'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { HeaderMenuPortal, useLinkProps } from '../../../../observability/public'; +import { AlertDropdown } from '../../alerting/log_threshold'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; -import { useLogSourceContext } from '../../containers/logs/log_source'; +import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; +import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params'; import { LogEntryCategoriesPage } from './log_entry_categories'; import { LogEntryRatePage } from './log_entry_rate'; import { LogsSettingsPage } from './settings'; import { StreamPage } from './stream'; -import { HeaderMenuPortal } from '../../../../observability/public'; -import { HeaderActionMenuContext } from '../../utils/header_action_menu_provider'; -import { useLinkProps } from '../../../../observability/public'; -import { useReadOnlyBadge } from '../../hooks/use_readonly_badge'; export const LogsPageContent: React.FunctionComponent = () => { const uiCapabilities = useKibana().services.application?.capabilities; const { setHeaderActionMenu, theme$ } = useContext(HeaderActionMenuContext); - const { initialize } = useLogSourceContext(); - const kibana = useKibana(); useReadOnlyBadge(!uiCapabilities?.logs?.save); - useMount(() => { - initialize(); - }); - // !! Need to be kept in sync with the deepLinks in x-pack/plugins/infra/public/plugin.ts const streamTab = { app: 'logs', diff --git a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx index 34ff237a9bd03..00f4935dc601b 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx @@ -6,21 +6,21 @@ */ import React from 'react'; -import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis'; -import { LogSourceProvider } from '../../containers/logs/log_source'; import { useSourceId } from '../../containers/source_id'; +import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; +import { LogViewProvider } from '../../hooks/use_log_view'; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { const [sourceId] = useSourceId(); const { services } = useKibanaContextForPlugin(); return ( - {children} - + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx index 5da03d9cb22c1..20b40f5599976 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/index_names_configuration_panel.tsx @@ -9,7 +9,7 @@ import { EuiCode, EuiDescribedFormGroup, EuiFieldText, EuiFormRow } from '@elast import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { LogIndexNameReference } from '../../../../common/log_sources'; +import { LogIndexNameReference } from '../../../../common/log_views'; import { FormElement } from './form_elements'; import { getFormRowProps, getInputFieldProps } from './form_field_props'; import { FormValidationError } from './validation_errors'; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx index 2d1c407595f61..0b9f9ceae99cf 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/index_pattern_configuration_panel.tsx @@ -9,7 +9,7 @@ import { EuiDescribedFormGroup, EuiFormRow, EuiLink, EuiSpacer } from '@elastic/ import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; -import { LogIndexPatternReference } from '../../../../common/log_sources'; +import { LogDataViewReference } from '../../../../common/log_views'; import { useLinkProps } from '../../../../../observability/public'; import { FormElement } from './form_elements'; import { getFormRowProps } from './form_field_props'; @@ -19,7 +19,7 @@ import { FormValidationError } from './validation_errors'; export const IndexPatternConfigurationPanel: React.FC<{ isLoading: boolean; isReadOnly: boolean; - indexPatternFormElement: FormElement; + indexPatternFormElement: FormElement; }> = ({ isLoading, isReadOnly, indexPatternFormElement }) => { useTrackPageview({ app: 'infra_logs', path: 'log_source_configuration_index_pattern' }); useTrackPageview({ @@ -29,11 +29,11 @@ export const IndexPatternConfigurationPanel: React.FC<{ }); const changeIndexPatternId = useCallback( - (indexPatternId: string | undefined) => { - if (indexPatternId != null) { + (dataViewId: string | undefined) => { + if (dataViewId != null) { indexPatternFormElement.updateValue(() => ({ - type: 'index_pattern', - indexPatternId, + type: 'data_view', + dataViewId, })); } else { indexPatternFormElement.updateValue(() => undefined); @@ -78,7 +78,7 @@ export const IndexPatternConfigurationPanel: React.FC<{ diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts index 1136524cf6c8d..8ebf2bed2b8df 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts @@ -6,13 +6,13 @@ */ import { useMemo } from 'react'; -import { SavedObjectNotFound } from '../../../../../../../src/plugins/kibana_utils/common'; -import { useUiTracker } from '../../../../../observability/public'; import { + LogDataViewReference, LogIndexNameReference, logIndexNameReferenceRT, - LogIndexPatternReference, -} from '../../../../common/log_sources'; +} from '../../../../common/log_views'; +import { SavedObjectNotFound } from '../../../../../../../src/plugins/kibana_utils/common'; +import { useUiTracker } from '../../../../../observability/public'; import { useKibanaIndexPatternService } from '../../../hooks/use_kibana_index_patterns'; import { useFormElement } from './form_elements'; import { @@ -21,7 +21,7 @@ import { validateStringNotEmpty, } from './validation_errors'; -export type LogIndicesFormState = LogIndexNameReference | LogIndexPatternReference | undefined; +export type LogIndicesFormState = LogIndexNameReference | LogDataViewReference | undefined; export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => { const indexPatternService = useKibanaIndexPatternService(); @@ -37,23 +37,20 @@ export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => { } else if (logIndexNameReferenceRT.is(logIndices)) { return validateStringNotEmpty('log indices', logIndices.indexName); } else { - const emptyStringErrors = validateStringNotEmpty( - 'log data view', - logIndices.indexPatternId - ); + const emptyStringErrors = validateStringNotEmpty('log data view', logIndices.dataViewId); if (emptyStringErrors.length > 0) { return emptyStringErrors; } const indexPatternErrors = await indexPatternService - .get(logIndices.indexPatternId) + .get(logIndices.dataViewId) .then(validateIndexPattern, (error): FormValidationError[] => { if (error instanceof SavedObjectNotFound) { return [ { type: 'missing_index_pattern' as const, - indexPatternId: logIndices.indexPatternId, + indexPatternId: logIndices.dataViewId, }, ]; } else { diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx index 46af94989f259..4127299d8db3b 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx @@ -11,10 +11,10 @@ import React, { useCallback } from 'react'; import { useUiTracker } from '../../../../../observability/public'; import { logIndexNameReferenceRT, - LogIndexPatternReference, - logIndexPatternReferenceRT, + LogDataViewReference, + logDataViewReferenceRT, LogIndexReference, -} from '../../../../common/log_sources'; +} from '../../../../common/log_views'; import { FormElement, isFormElementForType } from './form_elements'; import { IndexNamesConfigurationPanel } from './index_names_configuration_panel'; import { IndexPatternConfigurationPanel } from './index_pattern_configuration_panel'; @@ -28,7 +28,7 @@ export const IndicesConfigurationPanel = React.memo<{ const trackChangeIndexSourceType = useUiTracker({ app: 'infra_logs' }); const changeToIndexPatternType = useCallback(() => { - if (indicesFormElement.initialValue?.type === 'index_pattern') { + if (logDataViewReferenceRT.is(indicesFormElement.initialValue)) { indicesFormElement.updateValue(() => indicesFormElement.initialValue); } else { indicesFormElement.updateValue(() => undefined); @@ -83,11 +83,11 @@ export const IndicesConfigurationPanel = React.memo<{ } name="dataView" value="dataView" - checked={isIndexPatternFormElement(indicesFormElement)} + checked={isDataViewFormElement(indicesFormElement)} onChange={changeToIndexPatternType} disabled={isReadOnly} > - {isIndexPatternFormElement(indicesFormElement) && ( + {isDataViewFormElement(indicesFormElement) && ( - value == null || logIndexPatternReferenceRT.is(value) +const isDataViewFormElement = isFormElementForType( + (value): value is LogDataViewReference | undefined => + value == null || logDataViewReferenceRT.is(value) ); const isIndexNamesFormElement = isFormElementForType(logIndexNameReferenceRT.is); diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx index 6523708d18ee0..24537aadf183d 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx @@ -6,30 +6,28 @@ */ import { useMemo } from 'react'; -import { LogSourceConfigurationProperties } from '../../../containers/logs/log_source'; +import { LogViewAttributes } from '../../../../common/log_views'; import { useCompositeFormElement } from './form_elements'; import { useLogIndicesFormElement } from './indices_configuration_form_state'; import { useLogColumnsFormElement } from './log_columns_configuration_form_state'; import { useNameFormElement } from './name_configuration_form_state'; -export const useLogSourceConfigurationFormState = ( - configuration?: LogSourceConfigurationProperties -) => { - const nameFormElement = useNameFormElement(configuration?.name ?? ''); +export const useLogSourceConfigurationFormState = (logViewAttributes?: LogViewAttributes) => { + const nameFormElement = useNameFormElement(logViewAttributes?.name ?? ''); const logIndicesFormElement = useLogIndicesFormElement( useMemo( () => - configuration?.logIndices ?? { + logViewAttributes?.logIndices ?? { type: 'index_name', indexName: '', }, - [configuration] + [logViewAttributes] ) ); const logColumnsFormElement = useLogColumnsFormElement( - useMemo(() => configuration?.logColumns ?? [], [configuration]) + useMemo(() => logViewAttributes?.logColumns ?? [], [logViewAttributes]) ); const sourceConfigurationFormElement = useCompositeFormElement( diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx index daf4e73847163..afb8e1346bed6 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -17,18 +17,17 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useMemo } from 'react'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { useTrackPageview } from '../../../../../observability/public'; -import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; +import { Prompt, useTrackPageview } from '../../../../../observability/public'; import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; -import { Prompt } from '../../../../../observability/public'; +import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; +import { useLogViewContext } from '../../../hooks/use_log_view'; +import { settingsTitle } from '../../../translations'; +import { LogsPageTemplate } from '../page_template'; import { IndicesConfigurationPanel } from './indices_configuration_panel'; import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; import { NameConfigurationPanel } from './name_configuration_panel'; import { LogSourceConfigurationFormErrors } from './source_configuration_form_errors'; import { useLogSourceConfigurationFormState } from './source_configuration_form_state'; -import { LogsPageTemplate } from '../page_template'; -import { settingsTitle } from '../../../translations'; export const LogsSettingsPage = () => { const uiCapabilities = useKibana().services.application?.capabilities; @@ -47,18 +46,12 @@ export const LogsSettingsPage = () => { }, ]); - const { - sourceConfiguration: source, - hasFailedLoadingSource, - isLoading, - isUninitialized, - updateSource, - resolvedSourceConfiguration, - } = useLogSourceContext(); + const { logView, hasFailedLoadingLogView, isLoading, isUninitialized, update, resolvedLogView } = + useLogViewContext(); const availableFields = useMemo( - () => resolvedSourceConfiguration?.fields.map((field) => field.name) ?? [], - [resolvedSourceConfiguration] + () => resolvedLogView?.fields.map((field) => field.name) ?? [], + [resolvedLogView] ); const { @@ -67,22 +60,22 @@ export const LogsSettingsPage = () => { logIndicesFormElement, logColumnsFormElement, nameFormElement, - } = useLogSourceConfigurationFormState(source?.configuration); + } = useLogSourceConfigurationFormState(logView?.attributes); const persistUpdates = useCallback(async () => { - await updateSource(formState); + await update(formState); sourceConfigurationFormElement.resetValue(); - }, [updateSource, sourceConfigurationFormElement, formState]); + }, [update, sourceConfigurationFormElement, formState]); const isWriteable = useMemo( - () => shouldAllowEdit && source && source.origin !== 'internal', - [shouldAllowEdit, source] + () => shouldAllowEdit && logView && logView.origin !== 'internal', + [shouldAllowEdit, logView] ); - if ((isLoading || isUninitialized) && !resolvedSourceConfiguration) { + if ((isLoading || isUninitialized) && !resolvedLogView) { return ; } - if (hasFailedLoadingSource) { + if (hasFailedLoadingLogView) { return null; } diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx index 7f8ed4fa6a951..3b59e097e7f97 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -5,15 +5,15 @@ * 2.0. */ -import React from 'react'; import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { APP_WRAPPER_CLASS } from '../../../../../../../src/core/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; -import { LogsPageLogsContent } from './page_logs_content'; +import { useLogViewContext } from '../../../hooks/use_log_view'; import { LogsPageTemplate } from '../page_template'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; -import { APP_WRAPPER_CLASS } from '../../../../../../../src/core/public'; +import { LogsPageLogsContent } from './page_logs_content'; const streamTitle = i18n.translate('xpack.infra.logs.streamPageTitle', { defaultMessage: 'Stream', @@ -24,20 +24,20 @@ export const StreamPageContent: React.FunctionComponent = () => { hasFailedLoading, isLoading, isUninitialized, - loadSource, - latestLoadSourceFailures, - sourceStatus, - } = useLogSourceContext(); + latestLoadLogViewFailures, + load, + logViewStatus, + } = useLogViewContext(); if (isLoading || isUninitialized) { return ; } else if (hasFailedLoading) { - return ; + return ; } else { return ( { - const { resolvedSourceConfiguration, sourceConfiguration, sourceId } = useLogSourceContext(); + const { resolvedLogView, logView, logViewId } = useLogViewContext(); const { textScale, textWrap } = useContext(LogViewConfiguration.Context); const { surroundingLogsId, @@ -216,14 +216,12 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { logEntryId={flyoutLogEntryId} onCloseFlyout={closeLogEntryFlyout} onSetFieldFilter={setFilter} - sourceId={sourceId} + sourceId={logViewId} /> ) : null} - + { - const { derivedIndexPattern } = useLogSourceContext(); + const { derivedDataView } = useLogViewContext(); + return ( - + {children} @@ -28,7 +28,7 @@ const LogFilterStateProvider: React.FC = ({ children }) => { const ViewLogInContextProvider: React.FC = ({ children }) => { const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context); - const { sourceId } = useLogSourceContext(); + const { logViewId } = useLogViewContext(); if (!startTimestamp || !endTimestamp) { return null; @@ -38,7 +38,7 @@ const ViewLogInContextProvider: React.FC = ({ children }) => { {children} @@ -46,7 +46,7 @@ const ViewLogInContextProvider: React.FC = ({ children }) => { }; const LogEntriesStateProvider: React.FC = ({ children }) => { - const { sourceId } = useLogSourceContext(); + const { logViewId } = useLogViewContext(); const { startTimestamp, endTimestamp, targetPosition, isInitialized } = useContext( LogPositionState.Context ); @@ -65,7 +65,7 @@ const LogEntriesStateProvider: React.FC = ({ children }) => { return ( { }; const LogHighlightsStateProvider: React.FC = ({ children }) => { - const { sourceId, sourceConfiguration } = useLogSourceContext(); + const { logViewId, logView } = useLogViewContext(); const { topCursor, bottomCursor, entries } = useLogStreamContext(); const { filterQuery } = useContext(LogFilterState.Context); const highlightsProps = { - sourceId, - sourceVersion: sourceConfiguration?.version, + sourceId: logViewId, + sourceVersion: logView?.version, entriesStart: topCursor, entriesEnd: bottomCursor, centerCursor: entries.length > 0 ? entries[Math.floor(entries.length / 2)].cursor : null, @@ -94,10 +94,10 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => { }; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { - const { sourceStatus } = useLogSourceContext(); + const { logViewStatus } = useLogViewContext(); // The providers assume the source is loaded, so short-circuit them otherwise - if (sourceStatus?.logIndexStatus === 'missing') { + if (logViewStatus?.index === 'missing') { return <>{children}; } diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index fe036fd613fc9..706e8a98e997a 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -6,10 +6,11 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; -import { Query } from '@kbn/es-query'; import { QueryStringInput } from '../../../../../../../src/plugins/data/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogCustomizationMenu } from '../../../components/logging/log_customization_menu'; import { LogDatepicker } from '../../../components/logging/log_datepicker'; import { LogHighlightsMenu } from '../../../components/logging/log_highlights_menu'; @@ -19,12 +20,11 @@ import { LogFilterState } from '../../../containers/logs/log_filter'; import { LogFlyout } from '../../../containers/logs/log_flyout'; import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_highlights'; import { LogPositionState } from '../../../containers/logs/log_position'; -import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; -import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useLogViewContext } from '../../../hooks/use_log_view'; export const LogsToolbar = () => { - const { derivedIndexPattern } = useLogSourceContext(); + const { derivedDataView } = useLogViewContext(); const { availableTextScales, setTextScale, setTextWrap, textScale, textWrap } = useContext( LogViewConfiguration.Context ); @@ -57,7 +57,7 @@ export const LogsToolbar = () => { { setSurroundingLogsId(null); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 6a125c75ab396..623fab62f6286 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -10,10 +10,11 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public'; import { from } from 'rxjs'; import { map } from 'rxjs/operators'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; +import { defaultLogViewsStaticConfig } from '../common/log_views'; +import { InfraPublicConfig } from '../common/plugin_config_types'; import { createInventoryMetricRuleType } from './alerting/inventory'; import { createLogThresholdRuleType } from './alerting/log_threshold'; import { createMetricThresholdRuleType } from './alerting/metric_threshold'; -import type { CoreProvidersProps } from './apps/common_providers'; import { createLazyContainerMetricsTable } from './components/infrastructure_node_metrics_tables/container/create_lazy_container_metrics_table'; import { createLazyHostMetricsTable } from './components/infrastructure_node_metrics_tables/host/create_lazy_host_metrics_table'; import { createLazyPodMetricsTable } from './components/infrastructure_node_metrics_tables/pod/create_lazy_pod_metrics_table'; @@ -21,17 +22,29 @@ import { LOG_STREAM_EMBEDDABLE } from './components/log_stream/log_stream_embedd import { LogStreamEmbeddableFactoryDefinition } from './components/log_stream/log_stream_embeddable_factory'; import { createMetricsFetchData, createMetricsHasData } from './metrics_overview_fetchers'; import { registerFeatures } from './register_feature'; +import { LogViewsService } from './services/log_views'; import { InfraClientCoreSetup, InfraClientCoreStart, InfraClientPluginClass, InfraClientSetupDeps, InfraClientStartDeps, + InfraClientStartExports, + InfraClientStartServices, } from './types'; import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers'; export class Plugin implements InfraClientPluginClass { - constructor(_context: PluginInitializerContext) {} + public config: InfraPublicConfig; + private logViews: LogViewsService; + + constructor(context: PluginInitializerContext) { + this.config = context.config.get(); + this.logViews = new LogViewsService({ + messageFields: + this.config.sources?.default?.fields?.message ?? defaultLogViewsStaticConfig.messageFields, + }); + } setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) { if (pluginsSetup.home) { @@ -42,7 +55,9 @@ export class Plugin implements InfraClientPluginClass { createInventoryMetricRuleType() ); - pluginsSetup.observability.observabilityRuleTypeRegistry.register(createLogThresholdRuleType()); + pluginsSetup.observability.observabilityRuleTypeRegistry.register( + createLogThresholdRuleType(core) + ); pluginsSetup.observability.observabilityRuleTypeRegistry.register( createMetricThresholdRuleType() ); @@ -144,10 +159,10 @@ export class Plugin implements InfraClientPluginClass { category: DEFAULT_APP_CATEGORIES.observability, mount: async (params: AppMountParameters) => { // mount callback should not use setup dependencies, get start dependencies instead - const [coreStart, pluginsStart] = await core.getStartServices(); + const [coreStart, pluginsStart, pluginStart] = await core.getStartServices(); const { renderApp } = await import('./apps/logs_app'); - return renderApp(coreStart, pluginsStart, params); + return renderApp(coreStart, pluginsStart, pluginStart, params); }, }); @@ -186,10 +201,10 @@ export class Plugin implements InfraClientPluginClass { ], mount: async (params: AppMountParameters) => { // mount callback should not use setup dependencies, get start dependencies instead - const [coreStart, pluginsStart] = await core.getStartServices(); + const [coreStart, pluginsStart, pluginStart] = await core.getStartServices(); const { renderApp } = await import('./apps/metrics_app'); - return renderApp(coreStart, pluginsStart, params); + return renderApp(coreStart, pluginsStart, pluginStart, params); }, }); @@ -209,17 +224,22 @@ export class Plugin implements InfraClientPluginClass { } start(core: InfraClientCoreStart, plugins: InfraClientStartDeps) { - const coreProvidersProps: CoreProvidersProps = { - core, - plugins, - theme$: core.theme.theme$, - }; + const getStartServices = (): InfraClientStartServices => [core, plugins, startContract]; + + const logViews = this.logViews.start({ + http: core.http, + dataViews: plugins.dataViews, + search: plugins.data.search, + }); - return { - ContainerMetricsTable: createLazyContainerMetricsTable(coreProvidersProps), - HostMetricsTable: createLazyHostMetricsTable(coreProvidersProps), - PodMetricsTable: createLazyPodMetricsTable(coreProvidersProps), + const startContract: InfraClientStartExports = { + logViews, + ContainerMetricsTable: createLazyContainerMetricsTable(getStartServices), + HostMetricsTable: createLazyHostMetricsTable(getStartServices), + PodMetricsTable: createLazyPodMetricsTable(getStartServices), }; + + return startContract; } stop() {} diff --git a/x-pack/plugins/infra/server/routes/log_sources/index.ts b/x-pack/plugins/infra/public/services/log_views/index.ts similarity index 72% rename from x-pack/plugins/infra/server/routes/log_sources/index.ts rename to x-pack/plugins/infra/public/services/log_views/index.ts index 75163863db9e7..07bb7bfa88476 100644 --- a/x-pack/plugins/infra/server/routes/log_sources/index.ts +++ b/x-pack/plugins/infra/public/services/log_views/index.ts @@ -5,5 +5,6 @@ * 2.0. */ -export * from './configuration'; -export * from './status'; +export * from './log_views_client'; +export * from './log_views_service'; +export * from './types'; diff --git a/x-pack/plugins/infra/public/services/log_views/log_views_client.mock.ts b/x-pack/plugins/infra/public/services/log_views/log_views_client.mock.ts new file mode 100644 index 0000000000000..d2f8ada43c705 --- /dev/null +++ b/x-pack/plugins/infra/public/services/log_views/log_views_client.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ILogViewsClient } from './types'; + +export const createLogViewsClientMock = (): jest.Mocked => ({ + getLogView: jest.fn(), + getResolvedLogView: jest.fn(), + getResolvedLogViewStatus: jest.fn(), + putLogView: jest.fn(), + resolveLogView: jest.fn(), +}); diff --git a/x-pack/plugins/infra/public/services/log_views/log_views_client.ts b/x-pack/plugins/infra/public/services/log_views/log_views_client.ts new file mode 100644 index 0000000000000..75974e50351c6 --- /dev/null +++ b/x-pack/plugins/infra/public/services/log_views/log_views_client.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { HttpStart } from 'src/core/public'; +import { ISearchGeneric } from 'src/plugins/data/public'; +import { DataViewsContract } from 'src/plugins/data_views/public'; +import { + getLogViewResponsePayloadRT, + getLogViewUrl, + putLogViewRequestPayloadRT, +} from '../../../common/http_api/log_views'; +import { + FetchLogViewError, + FetchLogViewStatusError, + LogView, + LogViewAttributes, + LogViewsStaticConfig, + LogViewStatus, + PutLogViewError, + ResolvedLogView, + resolveLogView, +} from '../../../common/log_views'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { ILogViewsClient } from './types'; + +export class LogViewsClient implements ILogViewsClient { + constructor( + private readonly dataViews: DataViewsContract, + private readonly http: HttpStart, + private readonly search: ISearchGeneric, + private readonly config: LogViewsStaticConfig + ) {} + + public async getLogView(logViewId: string): Promise { + const response = await this.http.get(getLogViewUrl(logViewId)).catch((error) => { + throw new FetchLogViewError(`Failed to fetch log view "${logViewId}": ${error}`); + }); + + const { data } = decodeOrThrow( + getLogViewResponsePayloadRT, + (message: string) => + new FetchLogViewError(`Failed to decode log view "${logViewId}": ${message}"`) + )(response); + + return data; + } + + public async getResolvedLogView(logViewId: string): Promise { + const logView = await this.getLogView(logViewId); + const resolvedLogView = await this.resolveLogView(logView.attributes); + return resolvedLogView; + } + + public async getResolvedLogViewStatus(resolvedLogView: ResolvedLogView): Promise { + const indexStatus = await this.search({ + params: { + ignore_unavailable: true, + allow_no_indices: true, + index: resolvedLogView.indices, + size: 0, + terminate_after: 1, + track_total_hits: 1, + }, + }) + .toPromise() + .then( + ({ rawResponse }) => { + if (rawResponse._shards.total <= 0) { + return 'missing' as const; + } + + const totalHits = decodeTotalHits(rawResponse.hits.total); + if (typeof totalHits === 'number' ? totalHits > 0 : totalHits.value > 0) { + return 'available' as const; + } + + return 'empty' as const; + }, + (err) => { + if (err.status === 404) { + return 'missing' as const; + } + throw new FetchLogViewStatusError( + `Failed to check status of log indices of "${resolvedLogView.indices}": ${err}` + ); + } + ); + + return { + index: indexStatus, + }; + } + + public async putLogView( + logViewId: string, + logViewAttributes: Partial + ): Promise { + const response = await this.http + .put(getLogViewUrl(logViewId), { + body: JSON.stringify(putLogViewRequestPayloadRT.encode({ attributes: logViewAttributes })), + }) + .catch((error) => { + throw new PutLogViewError(`Failed to write log view "${logViewId}": ${error}`); + }); + + const { data } = decodeOrThrow( + getLogViewResponsePayloadRT, + (message: string) => + new PutLogViewError(`Failed to decode written log view "${logViewId}": ${message}"`) + )(response); + + return data; + } + + public async resolveLogView(logViewAttributes: LogViewAttributes): Promise { + return await resolveLogView(logViewAttributes, this.dataViews, this.config); + } +} + +const decodeTotalHits = decodeOrThrow( + rt.union([ + rt.number, + rt.type({ + value: rt.number, + }), + ]) +); diff --git a/x-pack/plugins/infra/public/services/log_views/log_views_service.mock.ts b/x-pack/plugins/infra/public/services/log_views/log_views_service.mock.ts new file mode 100644 index 0000000000000..2c0132447d0a8 --- /dev/null +++ b/x-pack/plugins/infra/public/services/log_views/log_views_service.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createLogViewsClientMock } from './log_views_client.mock'; +import { LogViewsServiceStart } from './types'; + +export const createLogViewsServiceStartMock = () => ({ + client: createLogViewsClientMock(), +}); + +export const _ensureTypeCompatibility = (): LogViewsServiceStart => + createLogViewsServiceStartMock(); diff --git a/x-pack/plugins/infra/public/services/log_views/log_views_service.ts b/x-pack/plugins/infra/public/services/log_views/log_views_service.ts new file mode 100644 index 0000000000000..9e081a8df5028 --- /dev/null +++ b/x-pack/plugins/infra/public/services/log_views/log_views_service.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogViewsStaticConfig } from '../../../common/log_views'; +import { LogViewsClient } from './log_views_client'; +import { LogViewsServiceStartDeps, LogViewsServiceSetup, LogViewsServiceStart } from './types'; + +export class LogViewsService { + constructor(private readonly config: LogViewsStaticConfig) {} + + public setup(): LogViewsServiceSetup {} + + public start({ dataViews, http, search }: LogViewsServiceStartDeps): LogViewsServiceStart { + const client = new LogViewsClient(dataViews, http, search.search, this.config); + + return { + client, + }; + } +} diff --git a/x-pack/plugins/infra/public/services/log_views/types.ts b/x-pack/plugins/infra/public/services/log_views/types.ts new file mode 100644 index 0000000000000..12dac93af6fa9 --- /dev/null +++ b/x-pack/plugins/infra/public/services/log_views/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpStart } from 'src/core/public'; +import { ISearchStart } from 'src/plugins/data/public'; +import { DataViewsContract } from 'src/plugins/data_views/public'; +import { + LogView, + LogViewAttributes, + LogViewStatus, + ResolvedLogView, +} from '../../../common/log_views'; + +export type LogViewsServiceSetup = void; + +export interface LogViewsServiceStart { + client: ILogViewsClient; +} + +export interface LogViewsServiceStartDeps { + dataViews: DataViewsContract; + http: HttpStart; + search: ISearchStart; +} + +export interface ILogViewsClient { + getLogView(logViewId: string): Promise; + getResolvedLogViewStatus(resolvedLogView: ResolvedLogView): Promise; + getResolvedLogView(logViewId: string): Promise; + putLogView(logViewId: string, logViewAttributes: Partial): Promise; + resolveLogView(logViewAttributes: LogViewAttributes): Promise; +} diff --git a/x-pack/plugins/infra/public/test_utils/entries.ts b/x-pack/plugins/infra/public/test_utils/entries.ts index f27ccae13cc81..4dc3732fd49d5 100644 --- a/x-pack/plugins/infra/public/test_utils/entries.ts +++ b/x-pack/plugins/infra/public/test_utils/entries.ts @@ -7,7 +7,7 @@ import faker from 'faker'; import { LogEntry } from '../../common/log_entry'; -import { LogSourceConfiguration } from '../containers/logs/log_source'; +import { LogViewColumnConfiguration } from '../../common/log_views'; export const ENTRIES_EMPTY = { data: { @@ -21,7 +21,7 @@ export function generateFakeEntries( count: number, startTimestamp: number, endTimestamp: number, - columns: LogSourceConfiguration['configuration']['logColumns'] + columns: LogViewColumnConfiguration[] ): LogEntry[] { const entries: LogEntry[] = []; const timestampStep = Math.floor((endTimestamp - startTimestamp) / count); diff --git a/x-pack/plugins/infra/public/test_utils/source_configuration.ts b/x-pack/plugins/infra/public/test_utils/source_configuration.ts deleted file mode 100644 index b327b76fc1d73..0000000000000 --- a/x-pack/plugins/infra/public/test_utils/source_configuration.ts +++ /dev/null @@ -1,51 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GetLogSourceConfigurationSuccessResponsePayload } from '../../common/http_api/log_sources'; - -export const DEFAULT_SOURCE_CONFIGURATION: GetLogSourceConfigurationSuccessResponsePayload = { - data: { - id: 'default', - version: 'WzQwNiwxXQ==', - updatedAt: 1608559663482, - origin: 'stored', - configuration: { - name: 'Default', - description: '', - logIndices: { - type: 'index_pattern', - indexPatternId: 'some-test-id', - }, - fields: { - container: 'container.id', - host: 'host.name', - pod: 'kubernetes.pod.uid', - tiebreaker: '_doc', - timestamp: '@timestamp', - message: ['message'], - }, - logColumns: [ - { - timestampColumn: { - id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f', - }, - }, - { - fieldColumn: { - id: ' eb9777a8-fcd3-420e-ba7d-172fff6da7a2', - field: 'event.dataset', - }, - }, - { - messageColumn: { - id: 'b645d6da-824b-4723-9a2a-e8cece1645c0', - }, - }, - ], - }, - }, -}; diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 4ac480484afbf..b4410b87d9b79 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -8,6 +8,7 @@ import type { CoreSetup, CoreStart, Plugin as PluginClass } from 'kibana/public'; import { IHttpFetchError } from 'src/core/public'; import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import type { DataViewsPublicPluginStart } from '../../../../src/plugins/data_views/public'; import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; @@ -27,15 +28,18 @@ import type { } from '../../observability/public'; // import type { OsqueryPluginStart } from '../../osquery/public'; import type { SpacesPluginStart } from '../../spaces/public'; +import { UnwrapPromise } from '../common/utility_types'; import type { SourceProviderProps, UseNodeMetricsTableOptions, } from './components/infrastructure_node_metrics_tables/shared'; +import { LogViewsServiceStart } from './services/log_views'; // Our own setup and start contract values export type InfraClientSetupExports = void; export interface InfraClientStartExports { + logViews: LogViewsServiceStart; ContainerMetricsTable: ( props: UseNodeMetricsTableOptions & Partial ) => JSX.Element; @@ -61,6 +65,7 @@ export interface InfraClientSetupDeps { export interface InfraClientStartDeps { data: DataPublicPluginStart; dataEnhanced: DataEnhancedStart; + dataViews: DataViewsPublicPluginStart; observability: ObservabilityPublicStart; spaces: SpacesPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; @@ -79,6 +84,8 @@ export type InfraClientPluginClass = PluginClass< InfraClientSetupDeps, InfraClientStartDeps >; +export type InfraClientStartServicesAccessor = InfraClientCoreSetup['getStartServices']; +export type InfraClientStartServices = UnwrapPromise>; export interface InfraHttpError extends IHttpFetchError { readonly body?: { diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index dd4bf2f8a8895..ffcffa29949e8 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -5,14 +5,11 @@ * 2.0. */ -import { encode } from 'rison-node'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { encode } from 'rison-node'; import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public'; import { DEFAULT_SOURCE_ID, TIMESTAMP_FIELD } from '../../common/constants'; -import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; -import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; -import { InfraClientCoreSetup, InfraClientStartDeps } from '../types'; -import { resolveLogSourceConfiguration } from '../../common/log_sources'; +import { InfraClientStartDeps, InfraClientStartServicesAccessor } from '../types'; interface StatsAggregation { buckets: Array<{ @@ -34,37 +31,32 @@ interface LogParams { type StatsAndSeries = Pick; -export function getLogsHasDataFetcher(getStartServices: InfraClientCoreSetup['getStartServices']) { +export function getLogsHasDataFetcher(getStartServices: InfraClientStartServicesAccessor) { return async () => { - const [core] = await getStartServices(); - const sourceStatus = await callFetchLogSourceStatusAPI(DEFAULT_SOURCE_ID, core.http.fetch); + const [, , { logViews }] = await getStartServices(); + const resolvedLogView = await logViews.client.getResolvedLogView(DEFAULT_SOURCE_ID); + const logViewStatus = await logViews.client.getResolvedLogViewStatus(resolvedLogView); + + const hasData = logViewStatus.index === 'available'; + const indices = resolvedLogView.indices; + return { - hasData: sourceStatus.data.logIndexStatus === 'available', - indices: sourceStatus.data.indices, + hasData, + indices, }; }; } export function getLogsOverviewDataFetcher( - getStartServices: InfraClientCoreSetup['getStartServices'] + getStartServices: InfraClientStartServicesAccessor ): FetchData { return async (params) => { - const [core, startPlugins] = await getStartServices(); - const { data } = startPlugins; - - const sourceConfiguration = await callFetchLogSourceConfigurationAPI( - DEFAULT_SOURCE_ID, - core.http.fetch - ); - - const resolvedLogSourceConfiguration = await resolveLogSourceConfiguration( - sourceConfiguration.data.configuration, - startPlugins.data.indexPatterns - ); + const [, { data }, { logViews }] = await getStartServices(); + const resolvedLogView = await logViews.client.getResolvedLogView(DEFAULT_SOURCE_ID); const { stats, series } = await fetchLogsOverview( { - index: resolvedLogSourceConfiguration.indices, + index: resolvedLogView.indices, }, params, data diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts index 1ae412a92e456..81077f82d048d 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts @@ -6,26 +6,14 @@ */ import { CoreStart } from 'kibana/public'; +import { of } from 'rxjs'; import { coreMock } from 'src/core/public/mocks'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; -import { createIndexPatternMock } from '../../common/dependency_mocks/index_patterns'; -import { GetLogSourceConfigurationSuccessResponsePayload } from '../../common/http_api/log_sources/get_log_source_configuration'; -import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; -import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; +import { createResolvedLogViewMock } from '../../common/log_views/resolved_log_view.mock'; +import { createInfraPluginStartMock } from '../mocks'; import { InfraClientStartDeps, InfraClientStartExports } from '../types'; import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './logs_overview_fetchers'; -jest.mock('../containers/logs/log_source/api/fetch_log_source_status'); -const mockedCallFetchLogSourceStatusAPI = callFetchLogSourceStatusAPI as jest.MockedFunction< - typeof callFetchLogSourceStatusAPI ->; - -jest.mock('../containers/logs/log_source/api/fetch_log_source_configuration'); -const mockedCallFetchLogSourceConfigurationAPI = - callFetchLogSourceConfigurationAPI as jest.MockedFunction< - typeof callFetchLogSourceConfigurationAPI - >; - const DEFAULT_PARAMS = { absoluteTime: { start: 1593430680000, end: 1593430800000 }, relativeTime: { start: 'now-2m', end: 'now' }, // Doesn't matter for the test @@ -36,155 +24,110 @@ const DEFAULT_PARAMS = { function setup() { const core = coreMock.createStart(); const data = dataPluginMock.createStartContract(); + const pluginStart = createInfraPluginStartMock(); + const pluginDeps = { data } as InfraClientStartDeps; - // `dataResponder.mockReturnValue()` will be the `response` in - // - // const searcher = data.search.getSearchStrategy('sth'); - // searcher.search(...).subscribe((**response**) => {}); - // - const dataResponder = jest.fn(); - - (data.indexPatterns.get as jest.Mock).mockResolvedValue( - createIndexPatternMock({ - id: 'test-index-pattern', - title: 'log-indices-*', - timeFieldName: '@timestamp', - type: undefined, - fields: [ - { - name: 'event.dataset', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - searchable: true, - }, - { - name: 'runtime_field', - type: 'string', - runtimeField: { - type: 'keyword', - script: { - source: 'emit("runtime value")', - }, - }, - esTypes: ['keyword'], - aggregatable: true, - searchable: true, - }, - ], - }) - ); - - (data.search.search as jest.Mock).mockReturnValue({ - subscribe: (progress: Function, error: Function, finish: Function) => { - progress(dataResponder()); - finish(); - }, - }); + const dataSearch = data.search.search as jest.MockedFunction; - const mockedGetStartServices = jest.fn(() => { - const deps = { data }; - return Promise.resolve([ - core as CoreStart, - deps as InfraClientStartDeps, - {} as InfraClientStartExports, - ]) as Promise<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>; - }); - return { core, mockedGetStartServices, dataResponder }; + const mockedGetStartServices = jest.fn(() => + Promise.resolve<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>([ + core, + pluginDeps, + pluginStart, + ]) + ); + return { core, dataSearch, mockedGetStartServices, pluginStart }; } describe('Logs UI Observability Homepage Functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('getLogsHasDataFetcher()', () => { - beforeEach(() => { - mockedCallFetchLogSourceStatusAPI.mockReset(); - }); it('should return true when non-empty indices exist', async () => { - const { mockedGetStartServices } = setup(); + const { mockedGetStartServices, pluginStart } = setup(); - mockedCallFetchLogSourceStatusAPI.mockResolvedValue({ - data: { logIndexStatus: 'available', indices: 'test-index' }, + pluginStart.logViews.client.getResolvedLogView.mockResolvedValue( + createResolvedLogViewMock({ indices: 'test-index' }) + ); + pluginStart.logViews.client.getResolvedLogViewStatus.mockResolvedValue({ + index: 'available', }); const hasData = getLogsHasDataFetcher(mockedGetStartServices); const response = await hasData(); - expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); + expect(pluginStart.logViews.client.getResolvedLogViewStatus).toHaveBeenCalledTimes(1); expect(response).toEqual({ hasData: true, indices: 'test-index' }); }); it('should return false when only empty indices exist', async () => { - const { mockedGetStartServices } = setup(); + const { mockedGetStartServices, pluginStart } = setup(); - mockedCallFetchLogSourceStatusAPI.mockResolvedValue({ - data: { logIndexStatus: 'empty', indices: 'test-index' }, + pluginStart.logViews.client.getResolvedLogView.mockResolvedValue( + createResolvedLogViewMock({ indices: 'test-index' }) + ); + pluginStart.logViews.client.getResolvedLogViewStatus.mockResolvedValue({ + index: 'empty', }); const hasData = getLogsHasDataFetcher(mockedGetStartServices); const response = await hasData(); - expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); + expect(pluginStart.logViews.client.getResolvedLogViewStatus).toHaveBeenCalledTimes(1); expect(response).toEqual({ hasData: false, indices: 'test-index' }); }); it('should return false when no index exists', async () => { - const { mockedGetStartServices } = setup(); + const { mockedGetStartServices, pluginStart } = setup(); - mockedCallFetchLogSourceStatusAPI.mockResolvedValue({ - data: { logIndexStatus: 'missing', indices: 'test-index' }, + pluginStart.logViews.client.getResolvedLogView.mockResolvedValue( + createResolvedLogViewMock({ indices: 'test-index' }) + ); + pluginStart.logViews.client.getResolvedLogViewStatus.mockResolvedValue({ + index: 'missing', }); const hasData = getLogsHasDataFetcher(mockedGetStartServices); const response = await hasData(); - expect(mockedCallFetchLogSourceStatusAPI).toHaveBeenCalledTimes(1); + expect(pluginStart.logViews.client.getResolvedLogViewStatus).toHaveBeenCalledTimes(1); expect(response).toEqual({ hasData: false, indices: 'test-index' }); }); }); describe('getLogsOverviewDataFetcher()', () => { - beforeAll(() => { - mockedCallFetchLogSourceConfigurationAPI.mockResolvedValue({ - data: { - configuration: { - logIndices: { - type: 'index_pattern', - indexPatternId: 'test-index-pattern', - }, - }, - }, - } as GetLogSourceConfigurationSuccessResponsePayload); - }); - - afterAll(() => { - mockedCallFetchLogSourceConfigurationAPI.mockReset(); - }); - it('should work', async () => { - const { mockedGetStartServices, dataResponder } = setup(); - - dataResponder.mockReturnValue({ - rawResponse: { - aggregations: { - stats: { - buckets: [ - { - key: 'nginx', - doc_count: 250, // Count is for 2 minutes - series: { - buckets: [ - // Counts are per 30 seconds - { key: 1593430680000, doc_count: 25 }, - { key: 1593430710000, doc_count: 50 }, - { key: 1593430740000, doc_count: 75 }, - { key: 1593430770000, doc_count: 100 }, - ], + const { mockedGetStartServices, dataSearch, pluginStart } = setup(); + + pluginStart.logViews.client.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + + dataSearch.mockReturnValue( + of({ + rawResponse: { + aggregations: { + stats: { + buckets: [ + { + key: 'nginx', + doc_count: 250, // Count is for 2 minutes + series: { + buckets: [ + // Counts are per 30 seconds + { key: 1593430680000, doc_count: 25 }, + { key: 1593430710000, doc_count: 50 }, + { key: 1593430740000, doc_count: 75 }, + { key: 1593430770000, doc_count: 100 }, + ], + }, }, - }, - ], + ], + }, }, }, - }, - }); + }) + ); const fetchData = getLogsOverviewDataFetcher(mockedGetStartServices); const response = await fetchData(DEFAULT_PARAMS); diff --git a/x-pack/plugins/infra/public/utils/use_observable.ts b/x-pack/plugins/infra/public/utils/use_observable.ts index 87d182c94ac05..dd89e1ce17edd 100644 --- a/x-pack/plugins/infra/public/utils/use_observable.ts +++ b/x-pack/plugins/infra/public/utils/use_observable.ts @@ -23,11 +23,10 @@ export const useObservable = < createObservableOnce: (inputValues: Observable) => OutputObservable, inputValues: InputValues ) => { - const [inputValues$] = useState(() => new BehaviorSubject(inputValues)); - const [output$] = useState(() => createObservableOnce(inputValues$)); + const [output$, next] = useBehaviorSubject(createObservableOnce, () => inputValues); useEffect(() => { - inputValues$.next(inputValues); + next(inputValues); // `inputValues` can't be statically analyzed // eslint-disable-next-line react-hooks/exhaustive-deps }, inputValues); @@ -35,6 +34,19 @@ export const useObservable = < return output$; }; +export const useBehaviorSubject = < + InputValue, + OutputValue, + OutputObservable extends Observable +>( + deriveObservableOnce: (input$: Observable) => OutputObservable, + createInitialValue: () => InputValue +) => { + const [subject$] = useState(() => new BehaviorSubject(createInitialValue())); + const [output$] = useState(() => deriveObservableOnce(subject$)); + return [output$, subject$.next.bind(subject$)] as const; +}; + export const useObservableState = ( state$: Observable, initialState: InitialState | (() => InitialState) diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 3e7ede11f7e9d..4c9b4ba531d80 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -13,6 +13,8 @@ import { METRIC_THRESHOLD_ALERT_TYPE_ID, } from '../common/alerting/metrics'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; +import { infraSourceConfigurationSavedObjectName } from './lib/sources/saved_object_type'; +import { logViewSavedObjectName } from './saved_objects'; export const METRICS_FEATURE = { id: METRICS_FEATURE_ID, @@ -92,7 +94,7 @@ export const LOGS_FEATURE = { catalogue: ['infralogging', 'logs'], api: ['infra'], savedObject: { - all: ['infrastructure-ui-source'], + all: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName], read: [], }, alerting: { @@ -125,7 +127,7 @@ export const LOGS_FEATURE = { }, savedObject: { all: [], - read: ['infrastructure-ui-source'], + read: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName], }, ui: ['show'], }, diff --git a/x-pack/plugins/infra/server/index.ts b/x-pack/plugins/infra/server/index.ts index 93be23356dfc3..dcaa3df2db23c 100644 --- a/x-pack/plugins/infra/server/index.ts +++ b/x-pack/plugins/infra/server/index.ts @@ -6,11 +6,11 @@ */ import { PluginInitializerContext } from 'src/core/server'; -import { config, InfraConfig, InfraServerPlugin, InfraPluginSetup } from './plugin'; +import { config, InfraConfig, InfraServerPlugin } from './plugin'; -export type { InfraConfig, InfraPluginSetup }; +export type { InfraPluginSetup, InfraPluginStart, InfraRequestHandlerContext } from './types'; +export type { InfraConfig }; export { config }; -export type { InfraRequestHandlerContext } from './types'; export function plugin(context: PluginInitializerContext) { return new InfraServerPlugin(context); diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index d289cf339851d..18f6c943f234b 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -5,9 +5,14 @@ * 2.0. */ -import { initIpToHostName } from './routes/ip_to_hostname'; import { InfraBackendLibs } from './lib/infra_types'; +import { initGetHostsAnomaliesRoute, initGetK8sAnomaliesRoute } from './routes/infra_ml'; +import { initInventoryMetaRoute } from './routes/inventory_metadata'; +import { initIpToHostName } from './routes/ip_to_hostname'; +import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; import { + initGetLogEntryAnomaliesDatasetsRoute, + initGetLogEntryAnomaliesRoute, initGetLogEntryCategoriesRoute, initGetLogEntryCategoryDatasetsRoute, initGetLogEntryCategoryDatasetsStatsRoute, @@ -15,27 +20,21 @@ import { initGetLogEntryExamplesRoute, initValidateLogAnalysisDatasetsRoute, initValidateLogAnalysisIndicesRoute, - initGetLogEntryAnomaliesRoute, - initGetLogEntryAnomaliesDatasetsRoute, } from './routes/log_analysis'; -import { initGetK8sAnomaliesRoute } from './routes/infra_ml'; -import { initGetHostsAnomaliesRoute } from './routes/infra_ml'; -import { initMetricExplorerRoute } from './routes/metrics_explorer'; -import { initMetricsAPIRoute } from './routes/metrics_api'; -import { initMetadataRoute } from './routes/metadata'; -import { initSnapshotRoute } from './routes/snapshot'; -import { initNodeDetailsRoute } from './routes/node_details'; import { initLogEntriesHighlightsRoute, - initLogEntriesSummaryRoute, initLogEntriesSummaryHighlightsRoute, + initLogEntriesSummaryRoute, } from './routes/log_entries'; -import { initInventoryMetaRoute } from './routes/inventory_metadata'; -import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; +import { initLogViewRoutes } from './routes/log_views'; +import { initMetadataRoute } from './routes/metadata'; +import { initMetricsAPIRoute } from './routes/metrics_api'; +import { initMetricExplorerRoute } from './routes/metrics_explorer'; import { initMetricsSourceConfigurationRoutes } from './routes/metrics_sources'; +import { initNodeDetailsRoute } from './routes/node_details'; import { initOverviewRoute } from './routes/overview'; -import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; import { initProcessListRoute } from './routes/process_list'; +import { initSnapshotRoute } from './routes/snapshot'; export const initInfraServer = (libs: InfraBackendLibs) => { initIpToHostName(libs); @@ -56,12 +55,11 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initLogEntriesHighlightsRoute(libs); initLogEntriesSummaryRoute(libs); initLogEntriesSummaryHighlightsRoute(libs); + initLogViewRoutes(libs); initMetricExplorerRoute(libs); initMetricsAPIRoute(libs); initMetadataRoute(libs); initInventoryMetaRoute(libs); - initLogSourceConfigurationRoutes(libs); - initLogSourceStatusRoutes(libs); initGetLogAlertsChartPreviewDataRoute(libs); initProcessListRoute(libs); initOverviewRoute(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 00c52dae7ed3c..7a0f44883f036 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 @@ -14,6 +14,7 @@ import { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, } from '../../../../../../../src/plugins/data/server'; +import { PluginStart as DataViewsPluginStart } from '../../../../../../../src/plugins/data_views/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_types/timeseries/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; @@ -36,6 +37,7 @@ export interface InfraServerPluginSetupDeps { export interface InfraServerPluginStartDeps { data: DataPluginStart; + dataViews: DataViewsPluginStart; } export interface CallWithRequestParams extends estypes.RequestBase { 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 7e8f5ebfd5af4..26c29b948e266 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 @@ -23,7 +23,7 @@ import { } from '../../domains/log_entries_domain'; import { SortedSearchHit } from '../framework'; import { KibanaFramework } from '../framework/kibana_framework_adapter'; -import { ResolvedLogSourceConfiguration } from '../../../../common/log_sources'; +import { ResolvedLogView } from '../../../../common/log_views'; import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../../../../common/constants'; const TIMESTAMP_FORMAT = 'epoch_millis'; @@ -33,7 +33,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { public async getLogEntries( requestContext: InfraPluginRequestHandlerContext, - resolvedLogSourceConfiguration: ResolvedLogSourceConfiguration, + resolvedLogView: ResolvedLogView, fields: string[], params: LogEntriesParams ): Promise<{ documents: LogEntryDocument[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { @@ -71,7 +71,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { const esQuery = { allow_no_indices: true, - index: resolvedLogSourceConfiguration.indices, + index: resolvedLogView.indices, ignore_unavailable: true, body: { size: size + 1, // Extra one to test if it has more before or after @@ -94,7 +94,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { ], }, }, - runtime_mappings: resolvedLogSourceConfiguration.runtimeMappings, + runtime_mappings: resolvedLogView.runtimeMappings, sort, ...highlightClause, ...searchAfterClause, @@ -127,7 +127,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { public async getContainedLogSummaryBuckets( requestContext: InfraPluginRequestHandlerContext, - resolvedLogSourceConfiguration: ResolvedLogSourceConfiguration, + resolvedLogView: ResolvedLogView, startTimestamp: number, endTimestamp: number, bucketSize: number, @@ -141,7 +141,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { const query = { allow_no_indices: true, - index: resolvedLogSourceConfiguration.indices, + index: resolvedLogView.indices, ignore_unavailable: true, body: { aggregations: { @@ -181,7 +181,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { ], }, }, - runtime_mappings: resolvedLogSourceConfiguration.runtimeMappings, + runtime_mappings: resolvedLogView.runtimeMappings, size: 0, track_total_hits: false, }, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 130ad69b111e9..b59235d3ea95a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -12,7 +12,7 @@ import { Logger } from '@kbn/logging'; import { InventoryMetricConditions } from '../../../../common/alerting/metrics'; import { InfraTimerangeInput } from '../../../../common/http_api'; import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; +import { LogQueryFields } from '../../metrics/types'; import { InfraSource } from '../../sources'; import { calcualteFromBasedOnMetric } from './lib/calculate_from_based_on_metric'; import { getData } from './lib/get_data'; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 289b4bc6ee74e..f962d73edebc4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -24,6 +24,7 @@ import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric import { SnapshotMetricType } from '../../../../common/inventory_models/types'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { InfraBackendLibs } from '../../infra_types'; +import { LogQueryFields } from '../../metrics/types'; import { buildErrorAlertReason, buildFiredAlertReason, @@ -64,7 +65,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = InventoryMetricThresholdAllowedActionGroups >(async ({ services, params, alertId, executionId, startedAt }) => { const startTime = Date.now(); - const { criteria, filterQuery, sourceId, nodeType, alertOnNoData } = params; + const { criteria, filterQuery, sourceId = 'default', nodeType, alertOnNoData } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const logger = createScopedLogger(libs.logger, 'inventoryRule', { alertId, executionId }); const { alertWithLifecycle, savedObjectsClient, getAlertStartedDate } = services; @@ -105,18 +106,16 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = return {}; } } - const source = await libs.sources.getSourceConfiguration( - savedObjectsClient, - sourceId || 'default' - ); + const source = await libs.sources.getSourceConfiguration(savedObjectsClient, sourceId); - const logQueryFields = await libs - .getLogQueryFields( - sourceId || 'default', - services.savedObjectsClient, - services.scopedClusterClient.asCurrentUser - ) - .catch(() => undefined); + const [, , { logViews }] = await libs.getStartServices(); + const logQueryFields: LogQueryFields | undefined = await logViews + .getClient(savedObjectsClient, services.scopedClusterClient.asCurrentUser) + .getResolvedLogView(sourceId) + .then( + ({ indices }) => ({ indexPattern: indices }), + () => undefined + ); const compositeSize = libs.configuration.alerting.inventory_threshold.group_by_page_size; const results = await Promise.all( diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts index 4f19f73231df6..c29c802f9391d 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/get_data.ts @@ -13,7 +13,7 @@ import { InventoryItemType, SnapshotMetricType, } from '../../../../../common/inventory_models/types'; -import { LogQueryFields } from '../../../../services/log_queries/get_log_query_fields'; +import { LogQueryFields } from '../../../metrics/types'; import { InfraSource } from '../../../sources'; import { createRequest } from './create_request'; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts index 7bf2cb5ea3394..56bbd69240dc2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts @@ -6,38 +6,38 @@ */ import { i18n } from '@kbn/i18n'; -import type { InfraPluginRequestHandlerContext } from '../../../types'; -import { KibanaFramework } from '../../adapters/framework/kibana_framework_adapter'; +import { + GroupedSearchQueryResponse, + GroupedSearchQueryResponseRT, + isOptimizedGroupedSearchQueryResponse, + UngroupedSearchQueryResponse, + UngroupedSearchQueryResponseRT, +} from '../../../../common/alerting/logs/log_threshold/types'; import { GetLogAlertsChartPreviewDataAlertParamsSubset, - Series, Point, + Series, } from '../../../../common/http_api/log_alerts'; +import { ResolvedLogView } from '../../../../common/log_views'; +import { decodeOrThrow } from '../../../../common/runtime_types'; +import type { InfraPluginRequestHandlerContext } from '../../../types'; +import { KibanaFramework } from '../../adapters/framework/kibana_framework_adapter'; import { + buildFiltersFromCriteria, getGroupedESQuery, getUngroupedESQuery, - buildFiltersFromCriteria, } from './log_threshold_executor'; -import { - UngroupedSearchQueryResponseRT, - UngroupedSearchQueryResponse, - GroupedSearchQueryResponse, - GroupedSearchQueryResponseRT, - isOptimizedGroupedSearchQueryResponse, -} from '../../../../common/alerting/logs/log_threshold/types'; -import { decodeOrThrow } from '../../../../common/runtime_types'; -import { ResolvedLogSourceConfiguration } from '../../../../common/log_sources'; const COMPOSITE_GROUP_SIZE = 40; export async function getChartPreviewData( requestContext: InfraPluginRequestHandlerContext, - resolvedLogSourceConfiguration: ResolvedLogSourceConfiguration, + resolvedLogView: ResolvedLogView, callWithRequest: KibanaFramework['callWithRequest'], alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset, buckets: number ) { - const { indices, timestampField, runtimeMappings } = resolvedLogSourceConfiguration; + const { indices, timestampField, runtimeMappings } = resolvedLogView; const { groupBy, timeSize, timeUnit } = alertParams; const isGrouped = groupBy && groupBy.length > 0 ? true : false; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 5bf1a914f49b1..b5bc3a15896cb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -42,7 +42,6 @@ import { UngroupedSearchQueryResponse, UngroupedSearchQueryResponseRT, } from '../../../../common/alerting/logs/log_threshold'; -import { resolveLogSourceConfiguration } from '../../../../common/log_sources'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { getLogsAppAlertUrl } from '../../../../common/formatters/alert_link'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; @@ -99,7 +98,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => >(async ({ services, params, startedAt }) => { const { alertWithLifecycle, savedObjectsClient, scopedClusterClient, getAlertStartedDate } = services; - const { sources, basePath } = libs; + const { basePath } = libs; const alertFactory: LogThresholdAlertFactory = (id, reason, value, threshold, actions) => { const alert = alertWithLifecycle({ @@ -131,16 +130,14 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => alert.replaceState({ alertState: AlertStates.ALERT, }); + return alert; }; - const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); - const { indices, timestampField, runtimeMappings } = await resolveLogSourceConfiguration( - sourceConfiguration.configuration, - await libs.framework.getIndexPatternsService( - savedObjectsClient, - scopedClusterClient.asCurrentUser - ) - ); + + const [, , { logViews }] = await libs.getStartServices(); + const { indices, timestampField, runtimeMappings } = await logViews + .getClient(savedObjectsClient, scopedClusterClient.asCurrentUser) + .getResolvedLogView('default'); // TODO: move to params try { const validatedParams = decodeOrThrow(ruleParamsRT)(params); 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 e7b1624206515..ef6b7122aa4de 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 @@ -7,39 +7,33 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { JsonObject } from '@kbn/utility-types'; - -import type { InfraPluginRequestHandlerContext } from '../../../types'; - import { LogEntriesSummaryBucket, LogEntriesSummaryHighlightsBucket, } from '../../../../common/http_api'; +import { LogColumn, LogEntry, LogEntryCursor } from '../../../../common/log_entry'; import { - LogSourceColumnConfiguration, - ResolvedLogSourceConfiguration, - resolveLogSourceConfiguration, -} from '../../../../common/log_sources'; -import { LogColumn, LogEntryCursor, LogEntry } from '../../../../common/log_entry'; -import { - InfraSourceConfiguration, - InfraSources, - SourceConfigurationFieldColumnRuntimeType, -} from '../../sources'; + LogViewColumnConfiguration, + logViewFieldColumnConfigurationRT, + ResolvedLogView, +} from '../../../../common/log_views'; +import { decodeOrThrow } from '../../../../common/runtime_types'; import { getBuiltinRules } from '../../../services/log_entries/message/builtin_rules'; import { CompiledLogMessageFormattingRule, + compileFormattingRules, Fields, Highlights, - compileFormattingRules, } from '../../../services/log_entries/message/message'; -import { KibanaFramework } from '../../adapters/framework/kibana_framework_adapter'; -import { decodeOrThrow } from '../../../../common/runtime_types'; +import type { InfraPluginRequestHandlerContext } from '../../../types'; +import { InfraBackendLibs } from '../../infra_types'; import { - logEntryDatasetsResponseRT, - LogEntryDatasetBucket, CompositeDatasetKey, createLogEntryDatasetsQuery, + LogEntryDatasetBucket, + logEntryDatasetsResponseRT, } from './queries/log_entry_datasets'; + export interface LogEntriesParams { startTimestamp: number; endTimestamp: number; @@ -66,17 +60,14 @@ const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; export class InfraLogEntriesDomain { constructor( private readonly adapter: LogEntriesAdapter, - private readonly libs: { - framework: KibanaFramework; - sources: InfraSources; - } + private readonly libs: Pick ) {} public async getLogEntriesAround( requestContext: InfraPluginRequestHandlerContext, sourceId: string, params: LogEntriesAroundParams, - columnOverrides?: LogSourceColumnConfiguration[] + columnOverrides?: LogViewColumnConfiguration[] ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { const { startTimestamp, endTimestamp, center, query, size, highlightTerm } = params; @@ -136,27 +127,26 @@ export class InfraLogEntriesDomain { requestContext: InfraPluginRequestHandlerContext, sourceId: string, params: LogEntriesParams, - columnOverrides?: LogSourceColumnConfiguration[] + columnOverrides?: LogViewColumnConfiguration[] ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { - const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const resolvedLogSourceConfiguration = await resolveLogSourceConfiguration( - configuration, - await this.libs.framework.getIndexPatternsServiceWithRequestContext(requestContext) - ); - const columnDefinitions = columnOverrides ?? configuration.logColumns; + const [, , { logViews }] = await this.libs.getStartServices(); + const resolvedLogView = await logViews + .getClient( + requestContext.core.savedObjects.client, + requestContext.core.elasticsearch.client.asCurrentUser + ) + .getResolvedLogView(sourceId); + const columnDefinitions = columnOverrides ?? resolvedLogView.columns; const messageFormattingRules = compileFormattingRules( - getBuiltinRules(configuration.fields.message) + getBuiltinRules(resolvedLogView.messageField) ); - const requiredFields = getRequiredFields(configuration, messageFormattingRules); + const requiredFields = getRequiredFields(resolvedLogView, messageFormattingRules); const { documents, hasMoreBefore, hasMoreAfter } = await this.adapter.getLogEntries( requestContext, - resolvedLogSourceConfiguration, + resolvedLogView, requiredFields, params ); @@ -201,17 +191,16 @@ export class InfraLogEntriesDomain { bucketSize: number, filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const resolvedLogSourceConfiguration = await resolveLogSourceConfiguration( - configuration, - await this.libs.framework.getIndexPatternsServiceWithRequestContext(requestContext) - ); + const [, , { logViews }] = await this.libs.getStartServices(); + const resolvedLogView = await logViews + .getClient( + requestContext.core.savedObjects.client, + requestContext.core.elasticsearch.client.asCurrentUser + ) + .getResolvedLogView(sourceId); const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( requestContext, - resolvedLogSourceConfiguration, + resolvedLogView, start, end, bucketSize, @@ -229,18 +218,17 @@ export class InfraLogEntriesDomain { highlightQueries: string[], filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const resolvedLogSourceConfiguration = await resolveLogSourceConfiguration( - configuration, - await this.libs.framework.getIndexPatternsServiceWithRequestContext(requestContext) - ); + const [, , { logViews }] = await this.libs.getStartServices(); + const resolvedLogView = await logViews + .getClient( + requestContext.core.savedObjects.client, + requestContext.core.elasticsearch.client.asCurrentUser + ) + .getResolvedLogView(sourceId); const messageFormattingRules = compileFormattingRules( - getBuiltinRules(configuration.fields.message) + getBuiltinRules(resolvedLogView.messageField) ); - const requiredFields = getRequiredFields(configuration, messageFormattingRules); + const requiredFields = getRequiredFields(resolvedLogView, messageFormattingRules); const summaries = await Promise.all( highlightQueries.map(async (highlightQueryPhrase) => { @@ -254,7 +242,7 @@ export class InfraLogEntriesDomain { : highlightQuery; const summaryBuckets = await this.adapter.getContainedLogSummaryBuckets( requestContext, - resolvedLogSourceConfiguration, + resolvedLogView, startTimestamp, endTimestamp, bucketSize, @@ -315,14 +303,14 @@ export class InfraLogEntriesDomain { export interface LogEntriesAdapter { getLogEntries( requestContext: InfraPluginRequestHandlerContext, - resolvedLogSourceConfiguration: ResolvedLogSourceConfiguration, + resolvedLogView: ResolvedLogView, fields: string[], params: LogEntriesParams ): Promise<{ documents: LogEntryDocument[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>; getContainedLogSummaryBuckets( requestContext: InfraPluginRequestHandlerContext, - resolvedLogSourceConfiguration: ResolvedLogSourceConfiguration, + resolvedLogView: ResolvedLogView, startTimestamp: number, endTimestamp: number, bucketSize: number, @@ -360,12 +348,12 @@ const convertLogSummaryBucketToSummaryHighlightBucket = ( }); const getRequiredFields = ( - configuration: InfraSourceConfiguration, + configuration: ResolvedLogView, messageFormattingRules: CompiledLogMessageFormattingRule ): string[] => { - const fieldsFromCustomColumns = configuration.logColumns.reduce( + const fieldsFromCustomColumns = configuration.columns.reduce( (accumulatedFields, logColumn) => { - if (SourceConfigurationFieldColumnRuntimeType.is(logColumn)) { + if (logViewFieldColumnConfigurationRT.is(logColumn)) { return [...accumulatedFields, logColumn.fieldColumn.field]; } return accumulatedFields; diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index bf2847491a901..0a627e9de9c35 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -8,9 +8,8 @@ import { Logger } from '@kbn/logging'; import type { IBasePath } from 'kibana/server'; import { handleEsError } from '../../../../../src/plugins/es_ui_shared/server'; -import { InfraConfig } from '../types'; -import { GetLogQueryFields } from '../services/log_queries/get_log_query_fields'; import { RulesServiceSetup } from '../services/rules'; +import { InfraConfig, InfraPluginStartServicesAccessor } from '../types'; import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; @@ -29,10 +28,10 @@ export interface InfraBackendLibs extends InfraDomainLibs { framework: KibanaFramework; sources: InfraSources; sourceStatus: InfraSourceStatus; - getLogQueryFields: GetLogQueryFields; handleEsError: typeof handleEsError; logsRules: RulesServiceSetup; metricsRules: RulesServiceSetup; + getStartServices: InfraPluginStartServicesAccessor; logger: Logger; basePath: IBasePath; } diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts index aca0483037912..feb0f6b5b9998 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts @@ -6,35 +6,39 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { InfraPluginRequestHandlerContext, InfraRequestHandlerContext } from '../../types'; -import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; -import { fetchMlJob, getLogEntryDatasets } from './common'; import { + AnomaliesSort, getJobId, - logEntryCategoriesJobTypes, - logEntryRateJobTypes, + isCategoryAnomaly, jobCustomSettingsRT, LogEntryAnomalyDatasets, - AnomaliesSort, + logEntryCategoriesJobTypes, + logEntryRateJobTypes, Pagination, - isCategoryAnomaly, } from '../../../common/log_analysis'; -import type { ResolvedLogSourceConfiguration } from '../../../common/log_sources'; -import type { MlSystem, MlAnomalyDetectors } from '../../types'; -import { createLogEntryAnomaliesQuery, logEntryAnomaliesResponseRT } from './queries'; +import { ResolvedLogView } from '../../../common/log_views'; +import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import type { + InfraPluginRequestHandlerContext, + InfraRequestHandlerContext, + MlAnomalyDetectors, + MlSystem, +} from '../../types'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; +import { fetchMlJob, getLogEntryDatasets } from './common'; import { InsufficientAnomalyMlJobsConfigured, InsufficientLogAnalysisMlJobConfigurationError, - UnknownCategoryError, isMlPrivilegesError, + UnknownCategoryError, } from './errors'; -import { decodeOrThrow } from '../../../common/runtime_types'; +import { fetchLogEntryCategories } from './log_entry_categories_analysis'; +import { createLogEntryAnomaliesQuery, logEntryAnomaliesResponseRT } from './queries'; import { createLogEntryExamplesQuery, logEntryExamplesResponseRT, } from './queries/log_entry_examples'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; -import { fetchLogEntryCategories } from './log_entry_categories_analysis'; interface MappedAnomalyHit { id: string; @@ -327,7 +331,7 @@ export async function getLogEntryExamples( endTime: number, dataset: string, exampleCount: number, - resolvedSourceConfiguration: ResolvedLogSourceConfiguration, + resolvedLogView: ResolvedLogView, callWithRequest: KibanaFramework['callWithRequest'], categoryId?: string ) { @@ -347,7 +351,7 @@ export async function getLogEntryExamples( const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings); const indices = customSettings?.logs_source_config?.indexPattern; const timestampField = customSettings?.logs_source_config?.timestampField; - const { tiebreakerField, runtimeMappings } = resolvedSourceConfiguration; + const { tiebreakerField, runtimeMappings } = resolvedLogView; if (indices == null || timestampField == null) { throw new InsufficientLogAnalysisMlJobConfigurationError( diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index 316f868589064..d4679c7919535 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -8,17 +8,18 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ElasticsearchClient } from 'src/core/server'; import { + CategoriesSort, compareDatasetsByMaximumAnomalyScore, getJobId, jobCustomSettingsRT, logEntryCategoriesJobTypes, - CategoriesSort, } from '../../../common/log_analysis'; import { LogEntryContext } from '../../../common/log_entry'; -import type { ResolvedLogSourceConfiguration } from '../../../common/log_sources'; +import { ResolvedLogView } from '../../../common/log_views'; import { startTracingSpan } from '../../../common/performance_tracing'; import { decodeOrThrow } from '../../../common/runtime_types'; import type { MlAnomalyDetectors, MlSystem } from '../../types'; +import { fetchMlJob, getLogEntryDatasets } from './common'; import { InsufficientLogAnalysisMlJobConfigurationError, UnknownCategoryError } from './errors'; import { createLogEntryCategoriesQuery, @@ -38,7 +39,6 @@ import { createTopLogEntryCategoriesQuery, topLogEntryCategoriesResponseRT, } from './queries/top_log_entry_categories'; -import { fetchMlJob, getLogEntryDatasets } from './common'; export async function getTopLogEntryCategories( context: { @@ -148,7 +148,7 @@ export async function getLogEntryCategoryExamples( endTime: number, categoryId: number, exampleCount: number, - resolvedSourceConfiguration: ResolvedLogSourceConfiguration + resolvedLogView: ResolvedLogView ) { const finalizeLogEntryCategoryExamplesSpan = startTracingSpan('get category example log entries'); @@ -166,7 +166,7 @@ export async function getLogEntryCategoryExamples( const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings); const indices = customSettings?.logs_source_config?.indexPattern; const timestampField = customSettings?.logs_source_config?.timestampField; - const { tiebreakerField, runtimeMappings } = resolvedSourceConfiguration; + const { tiebreakerField, runtimeMappings } = resolvedLogView; if (indices == null || timestampField == null) { throw new InsufficientLogAnalysisMlJobConfigurationError( diff --git a/x-pack/plugins/infra/server/lib/metrics/types.ts b/x-pack/plugins/infra/server/lib/metrics/types.ts index b18486f88cc4c..82e174f4f94c1 100644 --- a/x-pack/plugins/infra/server/lib/metrics/types.ts +++ b/x-pack/plugins/infra/server/lib/metrics/types.ts @@ -99,3 +99,7 @@ export type HistogramResponse = rt.TypeOf; export type GroupingResponse = rt.TypeOf; export type MetricsESResponse = HistogramResponse | GroupingResponse; + +export interface LogQueryFields { + indexPattern: string; +} diff --git a/x-pack/plugins/infra/server/lib/source_status.ts b/x-pack/plugins/infra/server/lib/source_status.ts index 2b04bf84c1546..8c6d7d1ad451f 100644 --- a/x-pack/plugins/infra/server/lib/source_status.ts +++ b/x-pack/plugins/infra/server/lib/source_status.ts @@ -7,7 +7,6 @@ import type { InfraPluginRequestHandlerContext } from '../types'; import { InfraSources } from './sources'; -import { ResolvedLogSourceConfiguration } from '../../common/log_sources'; export class InfraSourceStatus { constructor( @@ -43,16 +42,6 @@ export class InfraSourceStatus { ); return hasAlias; } - public async getLogIndexStatus( - requestContext: InfraPluginRequestHandlerContext, - resolvedLogSourceConfiguration: ResolvedLogSourceConfiguration - ): Promise { - const indexStatus = await this.adapter.getIndexStatus( - requestContext, - resolvedLogSourceConfiguration.indices - ); - return indexStatus; - } public async hasMetricIndices( requestContext: InfraPluginRequestHandlerContext, sourceId: string diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index db262a432b3fc..7fb40a502ec71 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -5,39 +5,4 @@ * 2.0. */ -import { METRICS_INDEX_PATTERN, LOGS_INDEX_PATTERN } from '../../../common/constants'; -import { InfraSourceConfiguration } from '../../../common/source_configuration/source_configuration'; - -export const defaultSourceConfiguration: InfraSourceConfiguration = { - name: 'Default', - description: '', - metricAlias: METRICS_INDEX_PATTERN, - logIndices: { - type: 'index_name', - indexName: LOGS_INDEX_PATTERN, - }, - fields: { - message: ['message', '@message'], - }, - inventoryDefaultView: '0', - metricsExplorerDefaultView: '0', - logColumns: [ - { - timestampColumn: { - id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f', - }, - }, - { - fieldColumn: { - id: ' eb9777a8-fcd3-420e-ba7d-172fff6da7a2', - field: 'event.dataset', - }, - }, - { - messageColumn: { - id: 'b645d6da-824b-4723-9a2a-e8cece1645c0', - }, - }, - ], - anomalyThreshold: 50, -}; +export { defaultSourceConfiguration } from '../../../common/source_configuration/defaults'; diff --git a/x-pack/plugins/infra/server/lib/sources/index.ts b/x-pack/plugins/infra/server/lib/sources/index.ts index 27ad665be31a9..73b50ac2662cc 100644 --- a/x-pack/plugins/infra/server/lib/sources/index.ts +++ b/x-pack/plugins/infra/server/lib/sources/index.ts @@ -6,6 +6,9 @@ */ export * from './defaults'; -export { infraSourceConfigurationSavedObjectType } from './saved_object_type'; +export { + infraSourceConfigurationSavedObjectName, + infraSourceConfigurationSavedObjectType, +} from './saved_object_type'; export * from './sources'; export * from '../../../common/source_configuration/source_configuration'; diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts index 1e3a3acce8926..ee65bd2d5ebeb 100644 --- a/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_references.ts @@ -10,61 +10,17 @@ import { InfraSavedSourceConfiguration, InfraSourceConfiguration, } from '../../../common/source_configuration/source_configuration'; +import { + SavedObjectAttributesWithReferences, + extractSavedObjectReferences as genericExtractSavedObjectReferences, + resolveSavedObjectReferences as genericResolveSavedObjectReferences, +} from '../../saved_objects/references'; import { SavedObjectReferenceResolutionError } from './errors'; export const logIndexPatternReferenceName = 'log_index_pattern_0'; export const inventoryDefaultViewReferenceName = 'inventory-saved-view-0'; export const metricsExplorerDefaultViewReferenceName = 'metrics-explorer-saved-view-0'; -interface SavedObjectAttributesWithReferences { - attributes: SavedObjectAttributes; - references: SavedObjectReference[]; -} - -/** - * Rewrites a source configuration such that well-known saved object references - * are extracted in the `references` array and replaced by the appropriate - * name. This is the inverse operation to `resolveSavedObjectReferences`. - */ -export const extractSavedObjectReferences = ( - sourceConfiguration: InfraSourceConfiguration -): SavedObjectAttributesWithReferences => - [ - extractLogIndicesSavedObjectReferences, - extractInventorySavedViewReferences, - extractMetricsExplorerSavedViewReferences, - ].reduce>( - ({ attributes: accumulatedAttributes, references: accumulatedReferences }, extract) => { - const { attributes, references } = extract(accumulatedAttributes); - return { - attributes, - references: [...accumulatedReferences, ...references], - }; - }, - { - attributes: sourceConfiguration, - references: [], - } - ); - -/** - * Rewrites a source configuration such that well-known saved object references - * are resolved from the `references` argument and replaced by the real saved - * object ids. This is the inverse operation to `extractSavedObjectReferences`. - */ -export const resolveSavedObjectReferences = ( - attributes: InfraSavedSourceConfiguration, - references: SavedObjectReference[] -): InfraSavedSourceConfiguration => - [ - resolveLogIndicesSavedObjectReferences, - resolveInventoryViewSavedObjectReferences, - resolveMetricsExplorerSavedObjectReferences, - ].reduce( - (accumulatedAttributes, resolve) => resolve(accumulatedAttributes, references), - attributes - ); - const extractLogIndicesSavedObjectReferences = ( sourceConfiguration: InfraSourceConfiguration ): SavedObjectAttributesWithReferences => { @@ -227,3 +183,25 @@ const resolveMetricsExplorerSavedObjectReferences = ( return attributes; } }; + +/** + * Rewrites a source configuration such that well-known saved object references + * are extracted in the `references` array and replaced by the appropriate + * name. This is the inverse operation to `resolveSavedObjectReferences`. + */ +export const extractSavedObjectReferences = genericExtractSavedObjectReferences([ + extractLogIndicesSavedObjectReferences, + extractInventorySavedViewReferences, + extractMetricsExplorerSavedViewReferences, +]); + +/** + * Rewrites a source configuration such that well-known saved object references + * are resolved from the `references` argument and replaced by the real saved + * object ids. This is the inverse operation to `extractSavedObjectReferences`. + */ +export const resolveSavedObjectReferences = genericResolveSavedObjectReferences([ + resolveLogIndicesSavedObjectReferences, + resolveInventoryViewSavedObjectReferences, + resolveMetricsExplorerSavedObjectReferences, +]); diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 4d2f0a8c4a159..7448b7cb8d7e0 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -236,7 +236,7 @@ export class InfraSources { } } -const mergeSourceConfiguration = ( +export const mergeSourceConfiguration = ( first: InfraSourceConfiguration, ...others: InfraStaticSourceConfiguration[] ) => diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 6b0400a0c5e65..e77f94dfe38f1 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -10,15 +10,18 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { Logger } from '@kbn/logging'; import { - CoreSetup, - PluginInitializerContext, + CoreStart, Plugin, PluginConfigDescriptor, + PluginInitializerContext, } from 'src/core/server'; +import { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; -import { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; +import { defaultLogViewsStaticConfig } from '../common/log_views'; +import { publicConfigKeys } from '../common/plugin_config_types'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; +import { configDeprecations, getInfraDeprecationsFactory } from './deprecations'; import { LOGS_FEATURE, METRICS_FEATURE } from './features'; import { initInfraServer } from './infra_server'; import { FrameworkFieldsAdapter } from './lib/adapters/fields/framework_fields_adapter'; @@ -34,13 +37,18 @@ 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 { logViewSavedObjectType } from './saved_objects'; import { LogEntriesService } from './services/log_entries'; -import { InfraPluginRequestHandlerContext, InfraConfig } from './types'; -import { UsageCollector } from './usage/usage_collector'; -import { createGetLogQueryFields } from './services/log_queries/get_log_query_fields'; -import { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; +import { LogViewsService } from './services/log_views'; import { RulesService } from './services/rules'; -import { configDeprecations, getInfraDeprecationsFactory } from './deprecations'; +import { + InfraConfig, + InfraPluginCoreSetup, + InfraPluginRequestHandlerContext, + InfraPluginSetup, + InfraPluginStart, +} from './types'; +import { UsageCollector } from './usage/usage_collector'; export const config: PluginConfigDescriptor = { schema: schema.object({ @@ -70,6 +78,7 @@ export const config: PluginConfigDescriptor = { ), }), deprecations: configDeprecations, + exposeToBrowser: publicConfigKeys, }; export type { InfraConfig }; @@ -82,23 +91,25 @@ const logsSampleDataLinkLabel = i18n.translate('xpack.infra.sampleDataLinkLabel' defaultMessage: 'Logs', }); -export interface InfraPluginSetup { - defineInternalSourceConfiguration: ( - sourceId: string, - sourceProperties: InfraStaticSourceConfiguration - ) => void; -} - -export class InfraServerPlugin implements Plugin { +export class InfraServerPlugin + implements + Plugin< + InfraPluginSetup, + InfraPluginStart, + InfraServerPluginSetupDeps, + InfraServerPluginStartDeps + > +{ public config: InfraConfig; - public libs: InfraBackendLibs | undefined; + public libs!: InfraBackendLibs; public logger: Logger; private logsRules: RulesService; private metricsRules: RulesService; + private logViews: LogViewsService; - constructor(context: PluginInitializerContext) { - this.config = context.config.get(); + constructor(context: PluginInitializerContext) { + this.config = context.config.get(); this.logger = context.logger.get(); this.logsRules = new RulesService( @@ -111,9 +122,11 @@ export class InfraServerPlugin implements Plugin { 'observability.metrics', this.logger.get('metricsRules') ); + + this.logViews = new LogViewsService(this.logger.get('logViews')); } - setup(core: CoreSetup, plugins: InfraServerPluginSetupDeps) { + setup(core: InfraPluginCoreSetup, plugins: InfraServerPluginSetupDeps) { const framework = new KibanaFramework(core, this.config, plugins); const sources = new InfraSources({ config: this.config, @@ -124,11 +137,13 @@ export class InfraServerPlugin implements Plugin { sources, } ); + const logViews = this.logViews.setup(); // register saved object types core.savedObjects.registerType(infraSourceConfigurationSavedObjectType); core.savedObjects.registerType(metricsExplorerViewSavedObjectType); core.savedObjects.registerType(inventoryViewSavedObjectType); + core.savedObjects.registerType(logViewSavedObjectType); // TODO: separate these out individually and do away with "domains" as a temporary group // and make them available via the request context so we can do away with @@ -139,7 +154,7 @@ export class InfraServerPlugin implements Plugin { }), logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), { framework, - sources, + getStartServices: () => core.getStartServices(), }), metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), }; @@ -150,10 +165,10 @@ export class InfraServerPlugin implements Plugin { sources, sourceStatus, ...domainLibs, - getLogQueryFields: createGetLogQueryFields(sources, framework), handleEsError, logsRules: this.logsRules.setup(core, plugins), metricsRules: this.metricsRules.setup(core, plugins), + getStartServices: () => core.getStartServices(), logger: this.logger, basePath: core.http.basePath, }; @@ -195,7 +210,7 @@ export class InfraServerPlugin implements Plugin { UsageCollector.registerUsageCollector(plugins.usageCollection); const logEntriesService = new LogEntriesService(); - logEntriesService.setup(core, { ...plugins, sources }); + logEntriesService.setup(core, plugins); // register deprecated source configuration fields core.deprecations.registerDeprecations({ @@ -203,12 +218,27 @@ export class InfraServerPlugin implements Plugin { }); return { - defineInternalSourceConfiguration(sourceId, sourceProperties) { - sources.defineInternalSourceConfiguration(sourceId, sourceProperties); - }, + defineInternalSourceConfiguration: sources.defineInternalSourceConfiguration.bind(sources), + logViews, } as InfraPluginSetup; } - start() {} + start(core: CoreStart, plugins: InfraServerPluginStartDeps) { + const logViews = this.logViews.start({ + infraSources: this.libs.sources, + savedObjects: core.savedObjects, + dataViews: plugins.dataViews, + elasticsearch: core.elasticsearch, + config: { + messageFields: + this.config.sources?.default?.fields?.message ?? + defaultLogViewsStaticConfig.messageFields, + }, + }); + + return { + logViews, + }; + } stop() {} } diff --git a/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts index 4cda9db3079e7..95b0c8320559e 100644 --- a/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts @@ -14,9 +14,11 @@ import { } from '../../../common/http_api/log_alerts/chart_preview_data'; import { createValidationFunction } from '../../../common/runtime_types'; import { getChartPreviewData } from '../../lib/alerting/log_threshold/log_threshold_chart_preview'; -import { resolveLogSourceConfiguration } from '../../../common/log_sources'; -export const initGetLogAlertsChartPreviewDataRoute = ({ framework, sources }: InfraBackendLibs) => { +export const initGetLogAlertsChartPreviewDataRoute = ({ + framework, + getStartServices, +}: Pick) => { framework.registerRoute( { method: 'post', @@ -30,20 +32,13 @@ export const initGetLogAlertsChartPreviewDataRoute = ({ framework, sources }: In data: { sourceId, buckets, alertParams }, } = request.body; - const { configuration } = await sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - - const resolvedLogSourceConfiguration = await resolveLogSourceConfiguration( - configuration, - await framework.getIndexPatternsServiceWithRequestContext(requestContext) - ); + const [, , { logViews }] = await getStartServices(); + const resolvedLogView = await logViews.getScopedClient(request).getResolvedLogView(sourceId); try { const { series } = await getChartPreviewData( requestContext, - resolvedLogSourceConfiguration, + resolvedLogView, framework.callWithRequest, alertParams, buckets diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts index 71558f97cf2bc..14787406319db 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts @@ -14,11 +14,13 @@ import { import { createValidationFunction } from '../../../../common/runtime_types'; import type { InfraBackendLibs } from '../../../lib/infra_types'; import { getLogEntryCategoryExamples } from '../../../lib/log_analysis'; -import { assertHasInfraMlPlugins } from '../../../utils/request_context'; import { isMlPrivilegesError } from '../../../lib/log_analysis/errors'; -import { resolveLogSourceConfiguration } from '../../../../common/log_sources'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; -export const initGetLogEntryCategoryExamplesRoute = ({ framework, sources }: InfraBackendLibs) => { +export const initGetLogEntryCategoryExamplesRoute = ({ + framework, + getStartServices, +}: Pick) => { framework.registerRoute( { method: 'post', @@ -37,14 +39,8 @@ export const initGetLogEntryCategoryExamplesRoute = ({ framework, sources }: Inf }, } = request.body; - const sourceConfiguration = await sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const resolvedSourceConfiguration = await resolveLogSourceConfiguration( - sourceConfiguration.configuration, - await framework.getIndexPatternsServiceWithRequestContext(requestContext) - ); + const [, , { logViews }] = await getStartServices(); + const resolvedLogView = await logViews.getScopedClient(request).getResolvedLogView(sourceId); try { assertHasInfraMlPlugins(requestContext); @@ -56,7 +52,7 @@ export const initGetLogEntryCategoryExamplesRoute = ({ framework, sources }: Inf endTime, categoryId, exampleCount, - resolvedSourceConfiguration + resolvedLogView ); return response.ok({ diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts index 83e6934d1b7a4..f11e9e46bf4c6 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_examples.ts @@ -6,19 +6,21 @@ */ import Boom from '@hapi/boom'; -import { createValidationFunction } from '../../../../common/runtime_types'; -import { InfraBackendLibs } from '../../../lib/infra_types'; -import { getLogEntryExamples } from '../../../lib/log_analysis'; -import { assertHasInfraMlPlugins } from '../../../utils/request_context'; import { getLogEntryExamplesRequestPayloadRT, getLogEntryExamplesSuccessReponsePayloadRT, LOG_ANALYSIS_GET_LOG_ENTRY_RATE_EXAMPLES_PATH, } from '../../../../common/http_api/log_analysis'; +import { createValidationFunction } from '../../../../common/runtime_types'; +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { getLogEntryExamples } from '../../../lib/log_analysis'; import { isMlPrivilegesError } from '../../../lib/log_analysis/errors'; -import { resolveLogSourceConfiguration } from '../../../../common/log_sources'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; -export const initGetLogEntryExamplesRoute = ({ framework, sources }: InfraBackendLibs) => { +export const initGetLogEntryExamplesRoute = ({ + framework, + getStartServices, +}: Pick) => { framework.registerRoute( { method: 'post', @@ -38,14 +40,8 @@ export const initGetLogEntryExamplesRoute = ({ framework, sources }: InfraBacken }, } = request.body; - const sourceConfiguration = await sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const resolvedSourceConfiguration = await resolveLogSourceConfiguration( - sourceConfiguration.configuration, - await framework.getIndexPatternsServiceWithRequestContext(requestContext) - ); + const [, , { logViews }] = await getStartServices(); + const resolvedLogView = await logViews.getScopedClient(request).getResolvedLogView(sourceId); try { assertHasInfraMlPlugins(requestContext); @@ -57,7 +53,7 @@ export const initGetLogEntryExamplesRoute = ({ framework, sources }: InfraBacken endTime, dataset, exampleCount, - resolvedSourceConfiguration, + resolvedLogView, framework.callWithRequest, categoryId ); diff --git a/x-pack/plugins/infra/server/routes/log_sources/configuration.ts b/x-pack/plugins/infra/server/routes/log_sources/configuration.ts deleted file mode 100644 index 9a92012c21fe4..0000000000000 --- a/x-pack/plugins/infra/server/routes/log_sources/configuration.ts +++ /dev/null @@ -1,117 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import { - getLogSourceConfigurationRequestParamsRT, - getLogSourceConfigurationSuccessResponsePayloadRT, - LOG_SOURCE_CONFIGURATION_PATH, - patchLogSourceConfigurationRequestBodyRT, - patchLogSourceConfigurationRequestParamsRT, - patchLogSourceConfigurationSuccessResponsePayloadRT, -} from '../../../common/http_api/log_sources'; -import { createValidationFunction } from '../../../common/runtime_types'; -import { InfraBackendLibs } from '../../lib/infra_types'; - -export const initLogSourceConfigurationRoutes = ({ framework, sources }: InfraBackendLibs) => { - framework.registerRoute( - { - method: 'get', - path: LOG_SOURCE_CONFIGURATION_PATH, - validate: { - params: createValidationFunction(getLogSourceConfigurationRequestParamsRT), - }, - }, - framework.router.handleLegacyErrors(async (requestContext, request, response) => { - const { sourceId } = request.params; - - try { - const sourceConfiguration = await sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - - return response.ok({ - body: getLogSourceConfigurationSuccessResponsePayloadRT.encode({ - data: sourceConfiguration, - }), - }); - } catch (error) { - if (Boom.isBoom(error)) { - throw error; - } - - return response.customError({ - statusCode: error.statusCode ?? 500, - body: { - message: error.message ?? 'An unexpected error occurred', - }, - }); - } - }) - ); - - framework.registerRoute( - { - method: 'patch', - path: LOG_SOURCE_CONFIGURATION_PATH, - validate: { - params: createValidationFunction(patchLogSourceConfigurationRequestParamsRT), - body: createValidationFunction(patchLogSourceConfigurationRequestBodyRT), - }, - }, - framework.router.handleLegacyErrors(async (requestContext, request, response) => { - const { sourceId } = request.params; - const { data: patchedSourceConfigurationProperties } = request.body; - - try { - const sourceConfiguration = await sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - - if (sourceConfiguration.origin === 'internal') { - response.conflict({ - body: 'A conflicting read-only source configuration already exists.', - }); - } - - const sourceConfigurationExists = sourceConfiguration.origin === 'stored'; - const patchedSourceConfiguration = await (sourceConfigurationExists - ? sources.updateSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId, - // @ts-ignore - patchedSourceConfigurationProperties - ) - : sources.createSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId, - // @ts-ignore - patchedSourceConfigurationProperties - )); - - return response.ok({ - body: patchLogSourceConfigurationSuccessResponsePayloadRT.encode({ - data: patchedSourceConfiguration, - }), - }); - } catch (error) { - if (Boom.isBoom(error)) { - throw error; - } - - return response.customError({ - statusCode: error.statusCode ?? 500, - body: { - message: error.message ?? 'An unexpected error occurred', - }, - }); - } - }) - ); -}; diff --git a/x-pack/plugins/infra/server/routes/log_sources/status.ts b/x-pack/plugins/infra/server/routes/log_sources/status.ts deleted file mode 100644 index e55e856483fc6..0000000000000 --- a/x-pack/plugins/infra/server/routes/log_sources/status.ts +++ /dev/null @@ -1,73 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import { - getLogSourceStatusRequestParamsRT, - getLogSourceStatusSuccessResponsePayloadRT, - LOG_SOURCE_STATUS_PATH, -} from '../../../common/http_api/log_sources'; -import { createValidationFunction } from '../../../common/runtime_types'; -import { InfraBackendLibs } from '../../lib/infra_types'; -import { resolveLogSourceConfiguration } from '../../../common/log_sources'; - -export const initLogSourceStatusRoutes = ({ - framework, - sourceStatus, - fields, - sources, -}: InfraBackendLibs) => { - framework.registerRoute( - { - method: 'get', - path: LOG_SOURCE_STATUS_PATH, - validate: { - params: createValidationFunction(getLogSourceStatusRequestParamsRT), - }, - }, - framework.router.handleLegacyErrors(async (requestContext, request, response) => { - const { sourceId } = request.params; - - try { - const sourceConfiguration = await sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - - const resolvedLogSourceConfiguration = await resolveLogSourceConfiguration( - sourceConfiguration.configuration, - await framework.getIndexPatternsServiceWithRequestContext(requestContext) - ); - - const logIndexStatus = await sourceStatus.getLogIndexStatus( - requestContext, - resolvedLogSourceConfiguration - ); - - return response.ok({ - body: getLogSourceStatusSuccessResponsePayloadRT.encode({ - data: { - logIndexStatus, - indices: resolvedLogSourceConfiguration.indices, - }, - }), - }); - } catch (error) { - if (Boom.isBoom(error)) { - throw error; - } - - return response.customError({ - statusCode: error.statusCode ?? 500, - body: { - message: error.message ?? 'An unexpected error occurred', - }, - }); - } - }) - ); -}; diff --git a/x-pack/plugins/infra/server/routes/log_views/get_log_view.ts b/x-pack/plugins/infra/server/routes/log_views/get_log_view.ts new file mode 100644 index 0000000000000..3f2bb8ec14427 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_views/get_log_view.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getLogViewRequestParamsRT, + getLogViewResponsePayloadRT, + LOG_VIEW_URL, +} from '../../../common/http_api/log_views'; +import { createValidationFunction } from '../../../common/runtime_types'; +import type { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_adapter'; +import type { InfraPluginStartServicesAccessor } from '../../types'; + +export const initGetLogViewRoute = ({ + framework, + getStartServices, +}: { + framework: KibanaFramework; + getStartServices: InfraPluginStartServicesAccessor; +}) => { + framework.registerRoute( + { + method: 'get', + path: LOG_VIEW_URL, + validate: { + params: createValidationFunction(getLogViewRequestParamsRT), + }, + }, + async (_requestContext, request, response) => { + const { logViewId } = request.params; + const { logViews } = (await getStartServices())[2]; + const logViewsClient = logViews.getScopedClient(request); + + try { + const logView = await logViewsClient.getLogView(logViewId); + + return response.ok({ + body: getLogViewResponsePayloadRT.encode({ + data: logView, + }), + }); + } catch (error) { + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/log_views/index.ts b/x-pack/plugins/infra/server/routes/log_views/index.ts new file mode 100644 index 0000000000000..fa7e6f6e1b9d3 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_views/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_adapter'; +import { InfraPluginStartServicesAccessor } from '../../types'; +import { initGetLogViewRoute } from './get_log_view'; +import { initPutLogViewRoute } from './put_log_view'; + +export const initLogViewRoutes = (dependencies: { + framework: KibanaFramework; + getStartServices: InfraPluginStartServicesAccessor; +}) => { + initGetLogViewRoute(dependencies); + initPutLogViewRoute(dependencies); +}; diff --git a/x-pack/plugins/infra/server/routes/log_views/put_log_view.ts b/x-pack/plugins/infra/server/routes/log_views/put_log_view.ts new file mode 100644 index 0000000000000..cf6eb74347310 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_views/put_log_view.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LOG_VIEW_URL, + putLogViewRequestParamsRT, + putLogViewRequestPayloadRT, + putLogViewResponsePayloadRT, +} from '../../../common/http_api/log_views'; +import { createValidationFunction } from '../../../common/runtime_types'; +import type { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_adapter'; +import type { InfraPluginStartServicesAccessor } from '../../types'; + +export const initPutLogViewRoute = ({ + framework, + getStartServices, +}: { + framework: KibanaFramework; + getStartServices: InfraPluginStartServicesAccessor; +}) => { + framework.registerRoute( + { + method: 'put', + path: LOG_VIEW_URL, + validate: { + params: createValidationFunction(putLogViewRequestParamsRT), + body: createValidationFunction(putLogViewRequestPayloadRT), + }, + }, + async (_requestContext, request, response) => { + const { logViewId } = request.params; + const { attributes } = request.body; + const { logViews } = (await getStartServices())[2]; + const logViewsClient = logViews.getScopedClient(request); + + try { + const logView = await logViewsClient.putLogView(logViewId, attributes); + + return response.ok({ + body: putLogViewResponsePayloadRT.encode({ + data: logView, + }), + }); + } catch (error) { + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index b86eb9f7d4c95..06b2104e0e17e 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -16,6 +16,7 @@ import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_ import { throwErrors } from '../../../common/runtime_types'; import { createSearchClient } from '../../lib/create_search_client'; import { getNodes } from './lib/get_nodes'; +import { LogQueryFields } from '../../lib/metrics/types'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); @@ -41,13 +42,14 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { snapshotRequest.sourceId ); const compositeSize = libs.configuration.inventory.compositeSize; - const logQueryFields = await libs - .getLogQueryFields( - snapshotRequest.sourceId, - requestContext.core.savedObjects.client, - requestContext.core.elasticsearch.client.asCurrentUser - ) - .catch(() => undefined); + const [, , { logViews }] = await libs.getStartServices(); + const logQueryFields: LogQueryFields | undefined = await logViews + .getScopedClient(request) + .getResolvedLogView(snapshotRequest.sourceId) + .then( + ({ indices }) => ({ indexPattern: indices }), + () => undefined + ); UsageCollector.countNode(snapshotRequest.nodeType); const client = createSearchClient(requestContext, framework); diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index a3ca2cfd683bb..7b75c5fd09980 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -6,13 +6,12 @@ */ import { SnapshotRequest } from '../../../../common/http_api'; -import { ESSearchClient } from '../../../lib/metrics/types'; +import { ESSearchClient, LogQueryFields } from '../../../lib/metrics/types'; import { InfraSource } from '../../../lib/sources'; import { transformRequestToMetricsAPIRequest } from './transform_request_to_metrics_api_request'; import { queryAllData } from './query_all_data'; import { transformMetricsApiResponseToSnapshotResponse } from './transform_metrics_ui_response'; import { copyMissingMetrics } from './copy_missing_metrics'; -import { LogQueryFields } from '../../../services/log_queries/get_log_query_fields'; export interface SourceOverrides { indexPattern: string; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/index.ts b/x-pack/plugins/infra/server/saved_objects/index.ts similarity index 89% rename from x-pack/plugins/infra/public/containers/logs/log_source/index.ts rename to x-pack/plugins/infra/server/saved_objects/index.ts index 6fa0d259ca9ce..bd7ecac5179a1 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/index.ts +++ b/x-pack/plugins/infra/server/saved_objects/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './log_source'; +export * from './log_view'; diff --git a/x-pack/plugins/infra/server/saved_objects/log_view/index.ts b/x-pack/plugins/infra/server/saved_objects/log_view/index.ts new file mode 100644 index 0000000000000..5d3011ed8bb32 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/log_view/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { logViewSavedObjectName, logViewSavedObjectType } from './log_view_saved_object'; +export { + extractLogViewSavedObjectReferences, + resolveLogViewSavedObjectReferences, +} from './references'; +export { logViewSavedObjectRT } from './types'; diff --git a/x-pack/plugins/infra/server/saved_objects/log_view/log_view_saved_object.ts b/x-pack/plugins/infra/server/saved_objects/log_view/log_view_saved_object.ts new file mode 100644 index 0000000000000..09b1098af6a61 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/log_view/log_view_saved_object.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { SavedObject, SavedObjectsType } from 'src/core/server'; +import { logViewSavedObjectRT } from './types'; + +export const logViewSavedObjectName = 'infrastructure-monitoring-log-view'; + +const getLogViewTitle = (savedObject: SavedObject) => + pipe( + logViewSavedObjectRT.decode(savedObject), + fold( + () => `Log view [id=${savedObject.id}]`, + ({ attributes: { name } }) => name + ) + ); + +export const logViewSavedObjectType: SavedObjectsType = { + name: logViewSavedObjectName, + hidden: false, + namespaceType: 'multiple-isolated', + management: { + defaultSearchField: 'name', + displayName: 'log view', + getTitle: getLogViewTitle, + icon: 'logsApp', + importableAndExportable: true, + }, + mappings: { + dynamic: false, + properties: { + name: { + type: 'text', + }, + }, + }, + migrations: {}, +}; diff --git a/x-pack/plugins/infra/server/saved_objects/log_view/references/index.ts b/x-pack/plugins/infra/server/saved_objects/log_view/references/index.ts new file mode 100644 index 0000000000000..fdfa2f6251731 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/log_view/references/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { extractSavedObjectReferences, resolveSavedObjectReferences } from '../../references'; +import { + extractLogIndicesSavedObjectReferences, + resolveLogIndicesSavedObjectReferences, +} from './log_indices'; + +export const extractLogViewSavedObjectReferences = extractSavedObjectReferences([ + extractLogIndicesSavedObjectReferences, +]); + +export const resolveLogViewSavedObjectReferences = resolveSavedObjectReferences([ + resolveLogIndicesSavedObjectReferences, +]); diff --git a/x-pack/plugins/infra/server/saved_objects/log_view/references/log_indices.ts b/x-pack/plugins/infra/server/saved_objects/log_view/references/log_indices.ts new file mode 100644 index 0000000000000..461bbccb6e414 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/log_view/references/log_indices.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectReference } from 'src/core/server'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../../../../../src/plugins/data_views/common'; +import { LogViewAttributes } from '../../../../common/log_views'; +import { + SavedObjectAttributesWithReferences, + SavedObjectReferenceResolutionError, +} from '../../references'; + +export const logIndicesDataViewReferenceName = 'log-indices-data-view-0'; + +export const extractLogIndicesSavedObjectReferences = ( + unextractedAttributes: LogViewAttributes +): SavedObjectAttributesWithReferences => { + if (unextractedAttributes.logIndices.type === 'data_view') { + const logDataViewReference: SavedObjectReference = { + id: unextractedAttributes.logIndices.dataViewId, + type: DATA_VIEW_SAVED_OBJECT_TYPE, + name: logIndicesDataViewReferenceName, + }; + const attributes: LogViewAttributes = { + ...unextractedAttributes, + logIndices: { + ...unextractedAttributes.logIndices, + dataViewId: logDataViewReference.name, + }, + }; + return { + attributes, + references: [logDataViewReference], + }; + } else { + return { + attributes: unextractedAttributes, + references: [], + }; + } +}; + +export const resolveLogIndicesSavedObjectReferences = ( + attributes: LogViewAttributes, + references: SavedObjectReference[] +): LogViewAttributes => { + if (attributes.logIndices?.type === 'data_view') { + const logDataViewReference = references.find( + (reference) => reference.name === logIndicesDataViewReferenceName + ); + + if (logDataViewReference == null) { + throw new SavedObjectReferenceResolutionError( + `Failed to resolve log data view reference "${logIndicesDataViewReferenceName}".` + ); + } + + return { + ...attributes, + logIndices: { + ...attributes.logIndices, + dataViewId: logDataViewReference.id, + }, + }; + } else { + return attributes; + } +}; diff --git a/x-pack/plugins/infra/server/saved_objects/log_view/types.ts b/x-pack/plugins/infra/server/saved_objects/log_view/types.ts new file mode 100644 index 0000000000000..2b19ad0470f7e --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/log_view/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isoToEpochRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; +import { logViewAttributesRT } from '../../../common/log_views'; +import { savedObjectReferenceRT } from '../references'; + +export const logViewSavedObjectRT = rt.intersection([ + rt.type({ + id: rt.string, + attributes: logViewAttributesRT, + references: rt.array(savedObjectReferenceRT), + }), + rt.partial({ + version: rt.string, + updated_at: isoToEpochRt, + }), +]); diff --git a/x-pack/plugins/infra/server/saved_objects/references.test.ts b/x-pack/plugins/infra/server/saved_objects/references.test.ts new file mode 100644 index 0000000000000..13675c03564a1 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/references.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectReference } from 'kibana/server'; +import { + extractSavedObjectReferences, + resolveSavedObjectReferences, + SavedObjectAttributesWithReferences, +} from './references'; + +it('extractSavedObjectReferences extracts references using the given extractors', () => { + const { attributes, references } = extractSavedObjectReferences([ + extractReferenceA, + extractReferenceB, + ])({ + a: 'id-a', + b: 'id-b', + c: 'something-else', + }); + + expect(references).toMatchObject([ + { id: 'id-a', name: REFERENCE_A_NAME, type: 'some-reference' }, + { id: 'id-b', name: REFERENCE_B_NAME, type: 'some-reference' }, + ]); + expect(attributes).toMatchObject({ + a: REFERENCE_A_NAME, + b: REFERENCE_B_NAME, + c: 'something-else', + }); +}); + +it('resolveSavedObjectReferences resolves references using the given resolvers', () => { + const attributes = resolveSavedObjectReferences([resolveReferenceA, resolveReferenceB])( + { + a: REFERENCE_A_NAME, + b: REFERENCE_B_NAME, + c: 'something-else', + }, + [ + { id: 'id-a', name: REFERENCE_A_NAME, type: 'some-reference' }, + { id: 'id-b', name: REFERENCE_B_NAME, type: 'some-reference' }, + ] + ); + + expect(attributes).toMatchObject({ + a: 'id-a', + b: 'id-b', + c: 'something-else', + }); +}); + +interface TestSavedObjectAttributes { + a: string; + b: string; + c: string; +} + +const REFERENCE_A_NAME = 'reference-a'; +const REFERENCE_B_NAME = 'reference-b'; + +const extractReferenceA = ( + attributes: TestSavedObjectAttributes +): SavedObjectAttributesWithReferences => ({ + attributes: { ...attributes, a: REFERENCE_A_NAME }, + references: [ + { + id: attributes.a, + name: REFERENCE_A_NAME, + type: 'some-reference', + }, + ], +}); + +const extractReferenceB = ( + attributes: TestSavedObjectAttributes +): SavedObjectAttributesWithReferences => ({ + attributes: { ...attributes, b: REFERENCE_B_NAME }, + references: [ + { + id: attributes.b, + name: REFERENCE_B_NAME, + type: 'some-reference', + }, + ], +}); + +const resolveReferenceA = ( + attributes: TestSavedObjectAttributes, + references: SavedObjectReference[] +): TestSavedObjectAttributes => { + const referenceA = references.find((reference) => reference.name === REFERENCE_A_NAME); + + if (referenceA != null) { + return { + ...attributes, + a: referenceA.id, + }; + } else { + return attributes; + } +}; + +const resolveReferenceB = ( + attributes: TestSavedObjectAttributes, + references: SavedObjectReference[] +): TestSavedObjectAttributes => { + const referenceB = references.find((reference) => reference.name === REFERENCE_B_NAME); + + if (referenceB != null) { + return { + ...attributes, + b: referenceB.id, + }; + } else { + return attributes; + } +}; diff --git a/x-pack/plugins/infra/server/saved_objects/references.ts b/x-pack/plugins/infra/server/saved_objects/references.ts new file mode 100644 index 0000000000000..3a2347faeb9db --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/references.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { SavedObject, SavedObjectReference } from 'src/core/server'; + +export type SavedObjectAttributesWithReferences = Pick< + SavedObject, + 'attributes' | 'references' +>; + +export type SavedObjectReferenceExtractor = ( + savedObjectAttributes: SavedObjectAttributes +) => SavedObjectAttributesWithReferences; + +export type SavedObjectReferenceResolver = ( + savedObjectAttributes: SavedObjectAttributes, + references: SavedObjectReference[] +) => SavedObjectAttributes; + +export const savedObjectReferenceRT = rt.strict({ + name: rt.string, + type: rt.string, + id: rt.string, +}); + +/** + * Rewrites a saved object such that well-known saved object references + * are extracted in the `references` array and replaced by the appropriate + * name. This is the inverse operation to `resolveSavedObjectReferences`. + */ +export const extractSavedObjectReferences = + ( + referenceExtractors: Array> + ) => + ( + savedObjectAttributes: SavedObjectAttributes + ): SavedObjectAttributesWithReferences => + referenceExtractors.reduce>( + ({ attributes: accumulatedAttributes, references: accumulatedReferences }, extract) => { + const { attributes, references } = extract(accumulatedAttributes); + return { + attributes, + references: [...accumulatedReferences, ...references], + }; + }, + { + attributes: savedObjectAttributes, + references: [], + } + ); + +/** + * Rewrites a source configuration such that well-known saved object references + * are resolved from the `references` argument and replaced by the real saved + * object ids. This is the inverse operation to `extractSavedObjectReferences`. + */ +export const resolveSavedObjectReferences = + ( + referenceResolvers: Array> + ) => + (attributes: SavedObjectAttributes, references: SavedObjectReference[]): SavedObjectAttributes => + referenceResolvers.reduce( + (accumulatedAttributes, resolve) => resolve(accumulatedAttributes, references), + attributes + ); + +export class SavedObjectReferenceResolutionError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'SavedObjectReferenceResolutionError'; + } +} diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts index cfa9e84fb3651..2f91c65205b55 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts @@ -20,12 +20,9 @@ import { SearchStrategyDependencies, } from 'src/plugins/data/server'; import { createSearchSessionsClientMock } from '../../../../../../src/plugins/data/server/search/mocks'; -import { - createIndexPatternMock, - createIndexPatternsStartMock, -} from '../../../common/dependency_mocks/index_patterns'; -import { InfraSource } from '../../lib/sources'; -import { createInfraSourcesMock } from '../../lib/sources/mocks'; +import { createResolvedLogViewMock } from '../../../common/log_views/resolved_log_view.mock'; +import { createLogViewsClientMock } from '../log_views/log_views_client.mock'; +import { createLogViewsServiceStartMock } from '../log_views/log_views_service.mock'; import { logEntriesSearchRequestStateRT, logEntriesSearchStrategyProvider, @@ -45,13 +42,15 @@ describe('LogEntries search strategy', () => { }); const dataMock = createDataPluginMock(esSearchStrategyMock); - const sourcesMock = createInfraSourcesMock(); - sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const logViewsClientMock = createLogViewsClientMock(); + logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + const logViewsMock = createLogViewsServiceStartMock(); + logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock); const mockDependencies = createSearchStrategyDependenciesMock(); const logEntriesSearchStrategy = logEntriesSearchStrategyProvider({ data: dataMock, - sources: sourcesMock, + logViews: logViewsMock, }); const response = await logEntriesSearchStrategy @@ -69,7 +68,8 @@ describe('LogEntries search strategy', () => { ) .toPromise(); - expect(sourcesMock.getSourceConfiguration).toHaveBeenCalled(); + expect(logViewsMock.getScopedClient).toHaveBeenCalled(); + expect(logViewsClientMock.getResolvedLogView).toHaveBeenCalled(); expect(esSearchStrategyMock.search).toHaveBeenCalledWith( expect.objectContaining({ params: expect.objectContaining({ @@ -124,13 +124,15 @@ describe('LogEntries search strategy', () => { }, }); const dataMock = createDataPluginMock(esSearchStrategyMock); - const sourcesMock = createInfraSourcesMock(); - sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const logViewsClientMock = createLogViewsClientMock(); + logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + const logViewsMock = createLogViewsServiceStartMock(); + logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock); const mockDependencies = createSearchStrategyDependenciesMock(); const logEntriesSearchStrategy = logEntriesSearchStrategyProvider({ data: dataMock, - sources: sourcesMock, + logViews: logViewsMock, }); const requestId = logEntriesSearchRequestStateRT.encode({ esRequestId: 'ASYNC_REQUEST_ID', @@ -152,7 +154,8 @@ describe('LogEntries search strategy', () => { ) .toPromise(); - expect(sourcesMock.getSourceConfiguration).toHaveBeenCalled(); + expect(logViewsMock.getScopedClient).toHaveBeenCalled(); + expect(logViewsClientMock.getResolvedLogView).toHaveBeenCalled(); expect(esSearchStrategyMock.search).toHaveBeenCalled(); expect(response.id).toEqual(requestId); expect(response.isRunning).toBe(false); @@ -205,13 +208,15 @@ describe('LogEntries search strategy', () => { }, }); const dataMock = createDataPluginMock(esSearchStrategyMock); - const sourcesMock = createInfraSourcesMock(); - sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const logViewsClientMock = createLogViewsClientMock(); + logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + const logViewsMock = createLogViewsServiceStartMock(); + logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock); const mockDependencies = createSearchStrategyDependenciesMock(); const logEntriesSearchStrategy = logEntriesSearchStrategyProvider({ data: dataMock, - sources: sourcesMock, + logViews: logViewsMock, }); const response = logEntriesSearchStrategy.search( @@ -243,13 +248,16 @@ describe('LogEntries search strategy', () => { }, }); const dataMock = createDataPluginMock(esSearchStrategyMock); - const sourcesMock = createInfraSourcesMock(); - sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const logViewsClientMock = createLogViewsClientMock(); + logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + const logViewsMock = createLogViewsServiceStartMock(); + logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock); + const mockDependencies = createSearchStrategyDependenciesMock(); const logEntriesSearchStrategy = logEntriesSearchStrategyProvider({ data: dataMock, - sources: sourcesMock, + logViews: logViewsMock, }); const requestId = logEntriesSearchRequestStateRT.encode({ esRequestId: 'ASYNC_REQUEST_ID', @@ -261,38 +269,6 @@ describe('LogEntries search strategy', () => { }); }); -const createSourceConfigurationMock = (): InfraSource => ({ - id: 'SOURCE_ID', - origin: 'stored' as const, - configuration: { - name: 'SOURCE_NAME', - description: 'SOURCE_DESCRIPTION', - logIndices: { - type: 'index_pattern', - indexPatternId: 'test-index-pattern', - }, - metricAlias: 'metric-indices-*', - inventoryDefaultView: 'DEFAULT_VIEW', - metricsExplorerDefaultView: 'DEFAULT_VIEW', - logColumns: [ - { timestampColumn: { id: 'TIMESTAMP_COLUMN_ID' } }, - { - fieldColumn: { - id: 'DATASET_COLUMN_ID', - field: 'event.dataset', - }, - }, - { - messageColumn: { id: 'MESSAGE_COLUMN_ID' }, - }, - ], - fields: { - message: ['MESSAGE_FIELD'], - }, - anomalyThreshold: 20, - }, -}); - const createEsSearchStrategyMock = (esSearchResponse: IEsSearchResponse) => ({ search: jest.fn((esSearchRequest: IEsSearchRequest) => { if (typeof esSearchRequest.id === 'string') { @@ -330,42 +306,4 @@ const createDataPluginMock = (esSearchStrategyMock: ISearchStrategy): any => ({ search: { getSearchStrategy: jest.fn().mockReturnValue(esSearchStrategyMock), }, - indexPatterns: createIndexPatternsStartMock(0, [ - createIndexPatternMock({ - id: 'test-index-pattern', - title: 'log-indices-*', - timeFieldName: '@timestamp', - type: undefined, - fields: [ - { - name: 'event.dataset', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - searchable: true, - }, - { - name: 'runtime_field', - type: 'string', - runtimeField: { - type: 'keyword', - script: { - source: 'emit("runtime value")', - }, - }, - esTypes: ['keyword'], - aggregatable: true, - searchable: true, - }, - ], - runtimeFields: { - runtime_field: { - type: 'keyword', - script: { - source: 'emit("runtime value")', - }, - }, - }, - }), - ]), }); diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts index b401b68d0e3d3..15f53d0236594 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.ts @@ -18,10 +18,6 @@ import type { ISearchStrategy, PluginStart as DataPluginStart, } from '../../../../../../src/plugins/data/server'; -import { - LogSourceColumnConfiguration, - logSourceFieldColumnConfigurationRT, -} from '../../../common/log_sources'; import { getLogEntryCursorFromHit, LogColumn, @@ -32,6 +28,10 @@ import { logEntryBeforeCursorRT, LogEntryContext, } from '../../../common/log_entry'; +import { + LogViewColumnConfiguration, + logViewFieldColumnConfigurationRT, +} from '../../../common/log_views'; import { decodeOrThrow } from '../../../common/runtime_types'; import { LogEntriesSearchRequestParams, @@ -39,12 +39,12 @@ import { LogEntriesSearchResponsePayload, logEntriesSearchResponsePayloadRT, } from '../../../common/search_strategies/log_entries/log_entries'; -import type { IInfraSources } from '../../lib/sources'; import { createAsyncRequestRTs, createErrorFromShardFailure, jsonFromBase64StringRT, } from '../../utils/typed_search_strategy'; +import { LogViewsServiceStart } from '../log_views/types'; import { CompiledLogMessageFormattingRule, compileFormattingRules, @@ -56,17 +56,16 @@ import { getSortDirection, LogEntryHit, } from './queries/log_entries'; -import { resolveLogSourceConfiguration } from '../../../common/log_sources'; type LogEntriesSearchRequest = IKibanaSearchRequest; type LogEntriesSearchResponse = IKibanaSearchResponse; export const logEntriesSearchStrategyProvider = ({ data, - sources, + logViews, }: { data: DataPluginStart; - sources: IInfraSources; + logViews: LogViewsServiceStart; }): ISearchStrategy => { const esSearchStrategy = data.search.getSearchStrategy('ese'); @@ -75,25 +74,12 @@ export const logEntriesSearchStrategyProvider = ({ defer(() => { const request = decodeOrThrow(asyncRequestRT)(rawRequest); - const resolvedSourceConfiguration$ = defer(() => - forkJoin([ - sources.getSourceConfiguration( - dependencies.savedObjectsClient, - request.params.sourceId - ), - data.indexPatterns.indexPatternsServiceFactory( - dependencies.savedObjectsClient, - dependencies.esClient.asCurrentUser - ), - ]).pipe( - concatMap(([sourceConfiguration, indexPatternsService]) => - resolveLogSourceConfiguration(sourceConfiguration.configuration, indexPatternsService) - ) - ) + const resolvedLogView$ = defer(() => + logViews.getScopedClient(dependencies.request).getResolvedLogView(request.params.sourceId) ).pipe(take(1), shareReplay(1)); const messageFormattingRules$ = defer(() => - resolvedSourceConfiguration$.pipe( + resolvedLogView$.pipe( map(({ messageField }) => compileFormattingRules(getBuiltinRules(messageField))) ) ).pipe(take(1), shareReplay(1)); @@ -106,7 +92,7 @@ export const logEntriesSearchStrategyProvider = ({ const initialRequest$ = of(request).pipe( filter(asyncInitialRequestRT.is), concatMap(({ params }) => - forkJoin([resolvedSourceConfiguration$, messageFormattingRules$]).pipe( + forkJoin([resolvedLogView$, messageFormattingRules$]).pipe( map( ([ { indices, timestampField, tiebreakerField, columns, runtimeMappings }, @@ -138,11 +124,7 @@ export const logEntriesSearchStrategyProvider = ({ concatMap((esRequest) => esSearchStrategy.search(esRequest, options, dependencies)) ); - return combineLatest([ - searchResponse$, - resolvedSourceConfiguration$, - messageFormattingRules$, - ]).pipe( + return combineLatest([searchResponse$, resolvedLogView$, messageFormattingRules$]).pipe( map(([esResponse, { columns }, messageFormattingRules]) => { const rawResponse = decodeOrThrow(getLogEntriesResponseRT)(esResponse.rawResponse); @@ -198,7 +180,7 @@ const { asyncInitialRequestRT, asyncRecoveredRequestRT, asyncRequestRT } = creat const getLogEntryFromHit = ( - columnDefinitions: LogSourceColumnConfiguration[], + columnDefinitions: LogViewColumnConfiguration[], messageFormattingRules: CompiledLogMessageFormattingRule ) => (hit: LogEntryHit): LogEntry => { @@ -271,11 +253,11 @@ function getResponseCursors(entries: LogEntry[]) { const VIEW_IN_CONTEXT_FIELDS = ['log.file.path', 'host.name', 'container.id']; const getRequiredFields = ( - columns: LogSourceColumnConfiguration[], + columns: LogViewColumnConfiguration[], messageFormattingRules: CompiledLogMessageFormattingRule ): string[] => { const fieldsFromColumns = columns.reduce((accumulatedFields, logColumn) => { - if (logSourceFieldColumnConfigurationRT.is(logColumn)) { + if (logViewFieldColumnConfigurationRT.is(logColumn)) { return [...accumulatedFields, logColumn.fieldColumn.field]; } return accumulatedFields; 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 index 12bde24761877..0c5e32a9514f3 100644 --- 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 @@ -10,21 +10,28 @@ import { LOG_ENTRY_SEARCH_STRATEGY } from '../../../common/search_strategies/log import { LOG_ENTRIES_SEARCH_STRATEGY } from '../../../common/search_strategies/log_entries/log_entries'; import { logEntriesSearchStrategyProvider } from './log_entries_search_strategy'; import { logEntrySearchStrategyProvider } from './log_entry_search_strategy'; -import { LogEntriesServiceSetupDeps, LogEntriesServiceStartDeps } from './types'; +import { + LogEntriesServiceSetupDeps, + LogEntriesServicePluginsStartDeps, + LogEntriesServicePluginSelfDeps, +} from './types'; export class LogEntriesService { - public setup(core: CoreSetup, setupDeps: LogEntriesServiceSetupDeps) { - core.getStartServices().then(([, startDeps]) => { + public setup( + core: CoreSetup, + setupDeps: LogEntriesServiceSetupDeps + ) { + core.getStartServices().then(([, startDeps, selfStartDeps]) => { setupDeps.data.search.registerSearchStrategy( LOG_ENTRIES_SEARCH_STRATEGY, - logEntriesSearchStrategyProvider({ ...setupDeps, ...startDeps }) + logEntriesSearchStrategyProvider({ ...setupDeps, ...startDeps, ...selfStartDeps }) ); setupDeps.data.search.registerSearchStrategy( LOG_ENTRY_SEARCH_STRATEGY, - logEntrySearchStrategyProvider({ ...setupDeps, ...startDeps }) + logEntrySearchStrategyProvider({ ...setupDeps, ...startDeps, ...selfStartDeps }) ); }); } - public start(_startDeps: LogEntriesServiceStartDeps) {} + public start(_startDeps: LogEntriesServicePluginsStartDeps) {} } 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 index c2f3c70580040..c6ee6fb92ca47 100644 --- 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 @@ -20,12 +20,9 @@ import { SearchStrategyDependencies, } from 'src/plugins/data/server'; import { createSearchSessionsClientMock } from '../../../../../../src/plugins/data/server/search/mocks'; -import { - createIndexPatternMock, - createIndexPatternsStartMock, -} from '../../../common/dependency_mocks/index_patterns'; -import { InfraSource } from '../../../common/source_configuration/source_configuration'; -import { createInfraSourcesMock } from '../../lib/sources/mocks'; +import { createResolvedLogViewMock } from '../../../common/log_views/resolved_log_view.mock'; +import { createLogViewsClientMock } from '../log_views/log_views_client.mock'; +import { createLogViewsServiceStartMock } from '../log_views/log_views_service.mock'; import { logEntrySearchRequestStateRT, logEntrySearchStrategyProvider, @@ -45,13 +42,15 @@ describe('LogEntry search strategy', () => { }); const dataMock = createDataPluginMock(esSearchStrategyMock); - const sourcesMock = createInfraSourcesMock(); - sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const logViewsClientMock = createLogViewsClientMock(); + logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + const logViewsMock = createLogViewsServiceStartMock(); + logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock); const mockDependencies = createSearchStrategyDependenciesMock(); const logEntrySearchStrategy = logEntrySearchStrategyProvider({ data: dataMock, - sources: sourcesMock, + logViews: logViewsMock, }); const response = await logEntrySearchStrategy @@ -64,7 +63,8 @@ describe('LogEntry search strategy', () => { ) .toPromise(); - expect(sourcesMock.getSourceConfiguration).toHaveBeenCalled(); + expect(logViewsMock.getScopedClient).toHaveBeenCalled(); + expect(logViewsClientMock.getResolvedLogView).toHaveBeenCalled(); expect(esSearchStrategyMock.search).toHaveBeenCalledWith( { params: expect.objectContaining({ @@ -123,13 +123,15 @@ describe('LogEntry search strategy', () => { }, }); const dataMock = createDataPluginMock(esSearchStrategyMock); - const sourcesMock = createInfraSourcesMock(); - sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const logViewsClientMock = createLogViewsClientMock(); + logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + const logViewsMock = createLogViewsServiceStartMock(); + logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock); const mockDependencies = createSearchStrategyDependenciesMock(); const logEntrySearchStrategy = logEntrySearchStrategyProvider({ data: dataMock, - sources: sourcesMock, + logViews: logViewsMock, }); const requestId = logEntrySearchRequestStateRT.encode({ esRequestId: 'ASYNC_REQUEST_ID', @@ -146,7 +148,8 @@ describe('LogEntry search strategy', () => { ) .toPromise(); - expect(sourcesMock.getSourceConfiguration).not.toHaveBeenCalled(); + expect(logViewsMock.getScopedClient).not.toHaveBeenCalled(); + expect(logViewsClientMock.getResolvedLogView).not.toHaveBeenCalled(); expect(esSearchStrategyMock.search).toHaveBeenCalled(); expect(response.id).toEqual(requestId); expect(response.isRunning).toBe(false); @@ -176,13 +179,15 @@ describe('LogEntry search strategy', () => { }, }); const dataMock = createDataPluginMock(esSearchStrategyMock); - const sourcesMock = createInfraSourcesMock(); - sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const logViewsClientMock = createLogViewsClientMock(); + logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + const logViewsMock = createLogViewsServiceStartMock(); + logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock); const mockDependencies = createSearchStrategyDependenciesMock(); const logEntrySearchStrategy = logEntrySearchStrategyProvider({ data: dataMock, - sources: sourcesMock, + logViews: logViewsMock, }); const response = logEntrySearchStrategy.search( @@ -209,13 +214,15 @@ describe('LogEntry search strategy', () => { }, }); const dataMock = createDataPluginMock(esSearchStrategyMock); - const sourcesMock = createInfraSourcesMock(); - sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const logViewsClientMock = createLogViewsClientMock(); + logViewsClientMock.getResolvedLogView.mockResolvedValue(createResolvedLogViewMock()); + const logViewsMock = createLogViewsServiceStartMock(); + logViewsMock.getScopedClient.mockReturnValue(logViewsClientMock); const mockDependencies = createSearchStrategyDependenciesMock(); const logEntrySearchStrategy = logEntrySearchStrategyProvider({ data: dataMock, - sources: sourcesMock, + logViews: logViewsMock, }); const requestId = logEntrySearchRequestStateRT.encode({ esRequestId: 'ASYNC_REQUEST_ID', @@ -227,27 +234,6 @@ describe('LogEntry search strategy', () => { }); }); -const createSourceConfigurationMock = (): InfraSource => ({ - id: 'SOURCE_ID', - origin: 'stored' as const, - configuration: { - name: 'SOURCE_NAME', - description: 'SOURCE_DESCRIPTION', - logIndices: { - type: 'index_pattern', - indexPatternId: 'test-index-pattern', - }, - metricAlias: 'metric-indices-*', - inventoryDefaultView: 'DEFAULT_VIEW', - metricsExplorerDefaultView: 'DEFAULT_VIEW', - logColumns: [], - fields: { - message: ['MESSAGE_FIELD'], - }, - anomalyThreshold: 20, - }, -}); - const createEsSearchStrategyMock = (esSearchResponse: IEsSearchResponse) => ({ search: jest.fn((esSearchRequest: IEsSearchRequest) => { if (typeof esSearchRequest.id === 'string') { @@ -285,42 +271,4 @@ const createDataPluginMock = (esSearchStrategyMock: ISearchStrategy): any => ({ search: { getSearchStrategy: jest.fn().mockReturnValue(esSearchStrategyMock), }, - indexPatterns: createIndexPatternsStartMock(0, [ - createIndexPatternMock({ - id: 'test-index-pattern', - title: 'log-indices-*', - timeFieldName: '@timestamp', - type: undefined, - fields: [ - { - name: 'event.dataset', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - searchable: true, - }, - { - name: 'runtime_field', - type: 'string', - runtimeField: { - type: 'keyword', - script: { - source: 'emit("runtime value")', - }, - }, - esTypes: ['keyword'], - aggregatable: true, - searchable: true, - }, - ], - runtimeFields: { - runtime_field: { - type: 'keyword', - script: { - source: 'emit("runtime value")', - }, - }, - }, - }), - ]), }); 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 index 565318578f990..6cd6e87810553 100644 --- 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 @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { concat, defer, of, forkJoin } from 'rxjs'; +import { concat, defer, of } from 'rxjs'; import { concatMap, filter, map, shareReplay, take } from 'rxjs/operators'; import type { IEsSearchRequest, @@ -25,24 +25,23 @@ import { 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 { LogViewsServiceStart } from '../log_views/types'; import { createGetLogEntryQuery, getLogEntryResponseRT, LogEntryHit } from './queries/log_entry'; -import { resolveLogSourceConfiguration } from '../../../common/log_sources'; type LogEntrySearchRequest = IKibanaSearchRequest; type LogEntrySearchResponse = IKibanaSearchResponse; export const logEntrySearchStrategyProvider = ({ data, - sources, + logViews, }: { data: DataPluginStart; - sources: IInfraSources; + logViews: LogViewsServiceStart; }): ISearchStrategy => { const esSearchStrategy = data.search.getSearchStrategy('ese'); @@ -51,21 +50,8 @@ export const logEntrySearchStrategyProvider = ({ defer(() => { const request = decodeOrThrow(asyncRequestRT)(rawRequest); - const resolvedSourceConfiguration$ = defer(() => - forkJoin([ - sources.getSourceConfiguration( - dependencies.savedObjectsClient, - request.params.sourceId - ), - data.indexPatterns.indexPatternsServiceFactory( - dependencies.savedObjectsClient, - dependencies.esClient.asCurrentUser - ), - ]).pipe( - concatMap(([sourceConfiguration, indexPatternsService]) => - resolveLogSourceConfiguration(sourceConfiguration.configuration, indexPatternsService) - ) - ) + const resolvedLogView$ = defer(() => + logViews.getScopedClient(dependencies.request).getResolvedLogView(request.params.sourceId) ).pipe(take(1), shareReplay(1)); const recoveredRequest$ = of(request).pipe( @@ -76,7 +62,7 @@ export const logEntrySearchStrategyProvider = ({ const initialRequest$ = of(request).pipe( filter(asyncInitialRequestRT.is), concatMap(({ params }) => - resolvedSourceConfiguration$.pipe( + resolvedLogView$.pipe( map( ({ indices, diff --git a/x-pack/plugins/infra/server/services/log_entries/types.ts b/x-pack/plugins/infra/server/services/log_entries/types.ts index 4d07acb1a64aa..e3ebe0eeece56 100644 --- a/x-pack/plugins/infra/server/services/log_entries/types.ts +++ b/x-pack/plugins/infra/server/services/log_entries/types.ts @@ -5,17 +5,20 @@ * 2.0. */ -import { +import type { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, } from '../../../../../../src/plugins/data/server'; -import { InfraSources } from '../../lib/sources'; +import type { LogViewsServiceStart } from '../log_views/types'; export interface LogEntriesServiceSetupDeps { data: DataPluginSetup; - sources: InfraSources; } -export interface LogEntriesServiceStartDeps { +export interface LogEntriesServicePluginsStartDeps { data: DataPluginStart; } + +export interface LogEntriesServicePluginSelfDeps { + logViews: LogViewsServiceStart; +} diff --git a/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts deleted file mode 100644 index db1696854db83..0000000000000 --- a/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts +++ /dev/null @@ -1,35 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; -import { InfraSources } from '../../lib/sources'; -import { resolveLogSourceConfiguration } from '../../../common/log_sources'; -import { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_adapter'; - -export interface LogQueryFields { - indexPattern: string; -} - -export const createGetLogQueryFields = (sources: InfraSources, framework: KibanaFramework) => { - return async ( - sourceId: string, - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient - ): Promise => { - const source = await sources.getSourceConfiguration(savedObjectsClient, sourceId); - const resolvedLogSourceConfiguration = await resolveLogSourceConfiguration( - source.configuration, - await framework.getIndexPatternsService(savedObjectsClient, elasticsearchClient) - ); - - return { - indexPattern: resolvedLogSourceConfiguration.indices, - }; - }; -}; - -export type GetLogQueryFields = ReturnType; diff --git a/x-pack/plugins/infra/server/services/log_views/errors.ts b/x-pack/plugins/infra/server/services/log_views/errors.ts new file mode 100644 index 0000000000000..fb0dc3b031511 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_views/errors.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export class NotFoundError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/plugins/infra/server/services/log_views/index.ts b/x-pack/plugins/infra/server/services/log_views/index.ts new file mode 100644 index 0000000000000..d9ec5cfcad261 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_views/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { LogViewsService } from './log_views_service'; +export { LogViewsClient } from './log_views_client'; +export type { LogViewsServiceSetup, LogViewsServiceStart, LogViewsServiceStartDeps } from './types'; diff --git a/x-pack/plugins/infra/server/services/log_views/log_views_client.mock.ts b/x-pack/plugins/infra/server/services/log_views/log_views_client.mock.ts new file mode 100644 index 0000000000000..ac69ae4f85ba5 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_views/log_views_client.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ILogViewsClient } from './types'; + +export const createLogViewsClientMock = (): jest.Mocked => ({ + getLogView: jest.fn(), + getResolvedLogView: jest.fn(), + putLogView: jest.fn(), + resolveLogView: jest.fn(), +}); diff --git a/x-pack/plugins/infra/server/services/log_views/log_views_client.test.ts b/x-pack/plugins/infra/server/services/log_views/log_views_client.test.ts new file mode 100644 index 0000000000000..c99930c7c47a5 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_views/log_views_client.test.ts @@ -0,0 +1,353 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggerMock } from '@kbn/logging-mocks'; +import { SavedObject, SavedObjectsUtils } from 'src/core/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { createStubDataView } from 'src/plugins/data_views/common/stubs'; +import { dataViewsService as dataViewsServiceMock } from 'src/plugins/data_views/server/mocks'; +import { + defaultLogViewId, + LogView, + LogViewAttributes, + LogViewsStaticConfig, +} from '../../../common/log_views'; +import { createLogViewMock } from '../../../common/log_views/log_view.mock'; +import { InfraSource } from '../../lib/sources'; +import { createInfraSourcesMock } from '../../lib/sources/mocks'; +import { + extractLogViewSavedObjectReferences, + logViewSavedObjectName, +} from '../../saved_objects/log_view'; +import { getAttributesFromSourceConfiguration, LogViewsClient } from './log_views_client'; + +describe('getAttributesFromSourceConfiguration function', () => { + it('converts the index_pattern log indices type to data_view', () => { + const logViewAttributes = getAttributesFromSourceConfiguration(basicTestSourceConfiguration); + + expect(logViewAttributes.logIndices).toEqual({ + type: 'data_view', + dataViewId: 'INDEX_PATTERN_ID', + }); + }); + + it('preserves the index_name log indices type', () => { + const logViewAttributes = getAttributesFromSourceConfiguration({ + ...basicTestSourceConfiguration, + configuration: { + ...basicTestSourceConfiguration.configuration, + logIndices: { + type: 'index_name', + indexName: 'INDEX_NAME', + }, + }, + }); + + expect(logViewAttributes.logIndices).toEqual({ + type: 'index_name', + indexName: 'INDEX_NAME', + }); + }); +}); + +describe('LogViewsClient class', () => { + it('getLogView resolves the default id to a real saved object id if it exists', async () => { + const { logViewsClient, savedObjectsClient } = createLogViewsClient(); + + const logViewMock = createLogViewMock('SAVED_OBJECT_ID'); + const logViewSavedObject: SavedObject = { + ...extractLogViewSavedObjectReferences(logViewMock.attributes), + id: logViewMock.id, + type: logViewSavedObjectName, + }; + + savedObjectsClient.get.mockResolvedValue(logViewSavedObject); + + savedObjectsClient.find.mockResolvedValue({ + total: 1, + saved_objects: [ + { + score: 0, + ...logViewSavedObject, + }, + ], + per_page: 1, + page: 1, + }); + + const logView = await logViewsClient.getLogView(defaultLogViewId); + + expect(savedObjectsClient.get).toHaveBeenCalledWith(logViewSavedObjectName, 'SAVED_OBJECT_ID'); + expect(logView).toEqual(logViewMock); + }); + + it('getLogView preserves non-default ids', async () => { + const { logViewsClient, savedObjectsClient } = createLogViewsClient(); + + const logViewMock = createLogViewMock('SAVED_OBJECT_ID'); + const logViewSavedObject: SavedObject = { + ...extractLogViewSavedObjectReferences(logViewMock.attributes), + id: logViewMock.id, + type: logViewSavedObjectName, + }; + + savedObjectsClient.get.mockResolvedValue(logViewSavedObject); + + savedObjectsClient.find.mockResolvedValue({ + total: 1, + saved_objects: [ + { + score: 0, + ...logViewSavedObject, + }, + ], + per_page: 1, + page: 1, + }); + + const logView = await logViewsClient.getLogView('SAVED_OBJECT_ID'); + + expect(savedObjectsClient.get).toHaveBeenCalledWith(logViewSavedObjectName, 'SAVED_OBJECT_ID'); + expect(logView).toEqual(logViewMock); + }); + + it('getLogView preserves the default id for fallback lookups', async () => { + const { infraSources, logViewsClient, savedObjectsClient } = createLogViewsClient(); + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + savedObjectsClient.find.mockResolvedValue({ + total: 0, + saved_objects: [], + per_page: 0, + page: 1, + }); + + await logViewsClient.getLogView(defaultLogViewId); + + expect(infraSources.getSourceConfiguration).toHaveBeenCalledWith( + savedObjectsClient, + defaultLogViewId + ); + }); + + it('putLogView resolves the default id to a real saved object id if one exists', async () => { + const { logViewsClient, savedObjectsClient } = createLogViewsClient(); + + const existingLogViewMock = createLogViewMock('SAVED_OBJECT_ID'); + const existingLogViewSavedObject: SavedObject = { + ...extractLogViewSavedObjectReferences(existingLogViewMock.attributes), + id: existingLogViewMock.id, + type: logViewSavedObjectName, + }; + + const newLogViewMock = createLogViewMock('SAVED_OBJECT_ID', 'stored', { name: 'New Log View' }); + const newLogViewSavedObject: SavedObject = { + ...extractLogViewSavedObjectReferences(newLogViewMock.attributes), + id: newLogViewMock.id, + type: logViewSavedObjectName, + }; + + savedObjectsClient.create.mockResolvedValue(newLogViewSavedObject); + + savedObjectsClient.find.mockResolvedValue({ + total: 1, + saved_objects: [ + { + score: 0, + ...existingLogViewSavedObject, + }, + ], + per_page: 1, + page: 1, + }); + + const logView = await logViewsClient.putLogView(defaultLogViewId, newLogViewMock.attributes); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + logViewSavedObjectName, + newLogViewMock.attributes, + expect.objectContaining({ id: 'SAVED_OBJECT_ID' }) + ); + expect(logView).toEqual(newLogViewMock); + }); + + it('putLogView resolves the default id to a new uuid if no default exists', async () => { + const { logViewsClient, savedObjectsClient } = createLogViewsClient(); + + const newLogViewMock = createLogViewMock('NOT_THE_FINAL_ID', 'stored', { + name: 'New Log View', + }); + const newLogViewSavedObject: SavedObject = { + ...extractLogViewSavedObjectReferences(newLogViewMock.attributes), + id: newLogViewMock.id, + type: logViewSavedObjectName, + }; + + savedObjectsClient.create.mockImplementation(async (_type, _attributes, { id = '' } = {}) => ({ + ...newLogViewSavedObject, + id, + })); + + savedObjectsClient.find.mockResolvedValue({ + total: 0, + saved_objects: [], + per_page: 0, + page: 1, + }); + + const logView = await logViewsClient.putLogView(defaultLogViewId, newLogViewMock.attributes); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + logViewSavedObjectName, + newLogViewMock.attributes, + expect.objectContaining({ + id: expect.any(String), // the id was generated + }) + ); + expect(logView).toEqual( + expect.objectContaining({ + ...newLogViewMock, + id: expect.any(String), // the id was generated + }) + ); + expect(SavedObjectsUtils.isRandomId(logView.id)).toBeTruthy(); + }); + + it('resolveLogView method resolves given LogViewAttributes with DataView reference', async () => { + const { logViewsClient, dataViews } = createLogViewsClient(); + + dataViews.get.mockResolvedValue( + createStubDataView({ + spec: { + id: 'LOG_DATA_VIEW', + title: 'log-indices-*', + timeFieldName: '@timestamp', + runtimeFieldMap: { + runtime_field: { + type: 'keyword', + script: { + source: 'emit("runtime value")', + }, + }, + }, + }, + }) + ); + + const resolvedLogView = await logViewsClient.resolveLogView({ + name: 'LOG VIEW', + description: 'LOG VIEW DESCRIPTION', + logIndices: { + type: 'data_view', + dataViewId: 'LOG_DATA_VIEW', + }, + logColumns: [ + { timestampColumn: { id: 'TIMESTAMP_COLUMN_ID' } }, + { + fieldColumn: { + id: 'DATASET_COLUMN_ID', + field: 'event.dataset', + }, + }, + { + messageColumn: { id: 'MESSAGE_COLUMN_ID' }, + }, + ], + }); + + expect(resolvedLogView).toMatchInlineSnapshot(` + Object { + "columns": Array [ + Object { + "timestampColumn": Object { + "id": "TIMESTAMP_COLUMN_ID", + }, + }, + Object { + "fieldColumn": Object { + "field": "event.dataset", + "id": "DATASET_COLUMN_ID", + }, + }, + Object { + "messageColumn": Object { + "id": "MESSAGE_COLUMN_ID", + }, + }, + ], + "description": "LOG VIEW DESCRIPTION", + "fields": FldList [], + "indices": "log-indices-*", + "messageField": Array [ + "message", + ], + "name": "LOG VIEW", + "runtimeMappings": Object { + "runtime_field": Object { + "script": Object { + "source": "emit(\\"runtime value\\")", + }, + "type": "keyword", + }, + }, + "tiebreakerField": "_doc", + "timestampField": "@timestamp", + } + `); + }); +}); + +const createLogViewsClient = () => { + const logger = loggerMock.create(); + const dataViews = dataViewsServiceMock; + const savedObjectsClient = savedObjectsClientMock.create(); + const infraSources = createInfraSourcesMock(); + const internalLogViews = new Map(); + const logViewStaticConfig: LogViewsStaticConfig = { + messageFields: ['message'], + }; + + const logViewsClient = new LogViewsClient( + logger, + Promise.resolve(dataViews), + savedObjectsClient, + infraSources, + internalLogViews, + logViewStaticConfig + ); + + return { + dataViews, + infraSources, + internalLogViews, + logViewStaticConfig, + logViewsClient, + savedObjectsClient, + }; +}; + +const basicTestSourceConfiguration: InfraSource = { + id: 'ID', + origin: 'stored', + configuration: { + name: 'NAME', + description: 'DESCRIPTION', + logIndices: { + type: 'index_pattern', + indexPatternId: 'INDEX_PATTERN_ID', + }, + logColumns: [], + fields: { + message: [], + }, + metricAlias: 'METRIC_ALIAS', + inventoryDefaultView: 'INVENTORY_DEFAULT_VIEW', + metricsExplorerDefaultView: 'METRICS_EXPLORER_DEFAULT_VIEW', + anomalyThreshold: 0, + }, +}; diff --git a/x-pack/plugins/infra/server/services/log_views/log_views_client.ts b/x-pack/plugins/infra/server/services/log_views/log_views_client.ts new file mode 100644 index 0000000000000..a4c64288d0ac0 --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_views/log_views_client.ts @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PluginStart as DataViewsServerPluginStart } from 'src/plugins/data_views/server'; +import { + Logger, + SavedObject, + SavedObjectsClientContract, + SavedObjectsUtils, +} from '../../../../../../src/core/server'; +import { + defaultLogViewAttributes, + defaultLogViewId, + LogIndexReference, + LogView, + LogViewAttributes, + LogViewsStaticConfig, + ResolvedLogView, + resolveLogView, +} from '../../../common/log_views'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { LogIndexReference as SourceConfigurationLogIndexReference } from '../../../common/source_configuration/source_configuration'; +import type { IInfraSources, InfraSource } from '../../lib/sources'; +import { + extractLogViewSavedObjectReferences, + logViewSavedObjectName, + resolveLogViewSavedObjectReferences, +} from '../../saved_objects/log_view'; +import { logViewSavedObjectRT } from '../../saved_objects/log_view/types'; +import { NotFoundError } from './errors'; +import { ILogViewsClient } from './types'; + +type DataViewsService = ReturnType; + +export class LogViewsClient implements ILogViewsClient { + static errors = { + NotFoundError, + }; + + constructor( + private readonly logger: Logger, + private readonly dataViews: DataViewsService, + private readonly savedObjectsClient: SavedObjectsClientContract, + private readonly infraSources: IInfraSources, + private readonly internalLogViews: Map, + private readonly config: LogViewsStaticConfig + ) {} + + public async getLogView(logViewId: string): Promise { + return await this.getSavedLogView(logViewId) + .catch((err) => + this.savedObjectsClient.errors.isNotFoundError(err) || err instanceof NotFoundError + ? this.getInternalLogView(logViewId) + : Promise.reject(err) + ) + .catch((err) => + err instanceof NotFoundError + ? this.getLogViewFromInfraSourceConfiguration(logViewId) + : Promise.reject(err) + ); + } + + public async getResolvedLogView(logViewId: string): Promise { + const logView = await this.getLogView(logViewId); + const resolvedLogView = await this.resolveLogView(logView.attributes); + return resolvedLogView; + } + + public async putLogView( + logViewId: string, + logViewAttributes: Partial + ): Promise { + const resolvedLogViewId = + (await this.resolveLogViewId(logViewId)) ?? SavedObjectsUtils.generateId(); + + this.logger.debug(`Trying to store log view "${logViewId}" as "${resolvedLogViewId}"...`); + + const logViewAttributesWithDefaults = { + ...defaultLogViewAttributes, + ...logViewAttributes, + }; + + const { attributes, references } = extractLogViewSavedObjectReferences( + logViewAttributesWithDefaults + ); + + const savedObject = await this.savedObjectsClient.create(logViewSavedObjectName, attributes, { + id: resolvedLogViewId, + overwrite: true, + references, + }); + + return getLogViewFromSavedObject(savedObject); + } + + public async resolveLogView(logViewAttributes: LogViewAttributes): Promise { + return await resolveLogView(logViewAttributes, await this.dataViews, this.config); + } + + private async getSavedLogView(logViewId: string): Promise { + this.logger.debug(`Trying to load stored log view "${logViewId}"...`); + + const resolvedLogViewId = await this.resolveLogViewId(logViewId); + + if (!resolvedLogViewId) { + throw new NotFoundError( + `Failed to load saved log view: the log view id "${logViewId}" could not be resolved.` + ); + } + + const savedObject = await this.savedObjectsClient.get( + logViewSavedObjectName, + resolvedLogViewId + ); + + return getLogViewFromSavedObject(savedObject); + } + + private async getInternalLogView(logViewId: string): Promise { + this.logger.debug(`Trying to load internal log view "${logViewId}"...`); + + const internalLogView = this.internalLogViews.get(logViewId); + + if (!internalLogView) { + throw new NotFoundError( + `Failed to load internal log view: no view with id "${logViewId}" found.` + ); + } + + return internalLogView; + } + + private async getLogViewFromInfraSourceConfiguration(sourceId: string): Promise { + this.logger.debug(`Trying to load log view from source configuration "${sourceId}"...`); + + const sourceConfiguration = await this.infraSources.getSourceConfiguration( + this.savedObjectsClient, + sourceId + ); + + return { + id: sourceConfiguration.id, + version: sourceConfiguration.version, + updatedAt: sourceConfiguration.updatedAt, + origin: `infra-source-${sourceConfiguration.origin}`, + attributes: getAttributesFromSourceConfiguration(sourceConfiguration), + }; + } + + private async resolveLogViewId(logViewId: string): Promise { + // only the default id needs to be transformed + if (logViewId !== defaultLogViewId) { + return logViewId; + } + + return await this.getNewestSavedLogViewId(); + } + + private async getNewestSavedLogViewId(): Promise { + const response = await this.savedObjectsClient.find({ + type: logViewSavedObjectName, + sortField: 'updated_at', + sortOrder: 'desc', + perPage: 1, + fields: [], + }); + + const [newestSavedLogView] = response.saved_objects; + + return newestSavedLogView?.id ?? null; + } +} + +const getLogViewFromSavedObject = (savedObject: SavedObject): LogView => { + const logViewSavedObject = decodeOrThrow(logViewSavedObjectRT)(savedObject); + + return { + id: logViewSavedObject.id, + version: logViewSavedObject.version, + updatedAt: logViewSavedObject.updated_at, + origin: 'stored', + attributes: resolveLogViewSavedObjectReferences( + logViewSavedObject.attributes, + savedObject.references + ), + }; +}; + +export const getAttributesFromSourceConfiguration = ({ + configuration: { name, description, logIndices, logColumns }, +}: InfraSource): LogViewAttributes => ({ + name, + description, + logIndices: getLogIndicesFromSourceConfigurationLogIndices(logIndices), + logColumns, +}); + +const getLogIndicesFromSourceConfigurationLogIndices = ( + logIndices: SourceConfigurationLogIndexReference +): LogIndexReference => + logIndices.type === 'index_pattern' + ? { + type: 'data_view', + dataViewId: logIndices.indexPatternId, + } + : logIndices; diff --git a/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts b/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts new file mode 100644 index 0000000000000..becd5a015b2ec --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_views/log_views_service.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createLogViewsClientMock } from './log_views_client.mock'; +import { LogViewsServiceStart } from './types'; + +export const createLogViewsServiceStartMock = (): jest.Mocked => ({ + getClient: jest.fn((_savedObjectsClient: any, _elasticsearchClient: any) => + createLogViewsClientMock() + ), + getScopedClient: jest.fn((_request: any) => createLogViewsClientMock()), +}); diff --git a/x-pack/plugins/infra/server/services/log_views/log_views_service.ts b/x-pack/plugins/infra/server/services/log_views/log_views_service.ts new file mode 100644 index 0000000000000..f5385b9d2873c --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_views/log_views_service.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ElasticsearchClient, + KibanaRequest, + Logger, + SavedObjectsClientContract, +} from 'src/core/server'; +import { LogView, LogViewAttributes } from '../../../common/log_views'; +import { LogViewsClient } from './log_views_client'; +import { LogViewsServiceSetup, LogViewsServiceStart, LogViewsServiceStartDeps } from './types'; + +export class LogViewsService { + private internalLogViews: Map = new Map(); + + constructor(private readonly logger: Logger) {} + + public setup(): LogViewsServiceSetup { + const { internalLogViews } = this; + + return { + defineInternalLogView(logViewId: string, logViewAttributes: LogViewAttributes) { + internalLogViews.set(logViewId, { + id: logViewId, + origin: 'internal', + attributes: logViewAttributes, + updatedAt: Date.now(), + }); + }, + }; + } + + public start({ + config, + dataViews, + elasticsearch, + infraSources, + savedObjects, + }: LogViewsServiceStartDeps): LogViewsServiceStart { + const { internalLogViews, logger } = this; + + return { + getClient( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient, + request?: KibanaRequest + ) { + return new LogViewsClient( + logger, + dataViews.dataViewsServiceFactory(savedObjectsClient, elasticsearchClient, request), + savedObjectsClient, + infraSources, + internalLogViews, + config + ); + }, + getScopedClient(request: KibanaRequest) { + const savedObjectsClient = savedObjects.getScopedClient(request); + const elasticsearchClient = elasticsearch.client.asScoped(request).asCurrentUser; + + return this.getClient(savedObjectsClient, elasticsearchClient, request); + }, + }; + } +} diff --git a/x-pack/plugins/infra/server/services/log_views/types.ts b/x-pack/plugins/infra/server/services/log_views/types.ts new file mode 100644 index 0000000000000..0052db19ee4de --- /dev/null +++ b/x-pack/plugins/infra/server/services/log_views/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ElasticsearchClient, + ElasticsearchServiceStart, + KibanaRequest, + SavedObjectsClientContract, + SavedObjectsServiceStart, +} from 'src/core/server'; +import { PluginStart as DataViewsServerPluginStart } from 'src/plugins/data_views/server'; +import { + LogView, + LogViewAttributes, + LogViewsStaticConfig, + ResolvedLogView, +} from '../../../common/log_views'; +import { InfraSources } from '../../lib/sources'; + +export interface LogViewsServiceStartDeps { + config: LogViewsStaticConfig; + dataViews: DataViewsServerPluginStart; + elasticsearch: ElasticsearchServiceStart; + infraSources: InfraSources; + savedObjects: SavedObjectsServiceStart; +} + +export interface LogViewsServiceSetup { + defineInternalLogView(logViewId: string, logViewAttributes: LogViewAttributes): void; +} + +export interface LogViewsServiceStart { + getClient( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient, + request?: KibanaRequest + ): ILogViewsClient; + getScopedClient(request: KibanaRequest): ILogViewsClient; +} + +export interface ILogViewsClient { + getLogView(logViewId: string): Promise; + getResolvedLogView(logViewId: string): Promise; + putLogView(logViewId: string, logViewAttributes: Partial): Promise; + resolveLogView(logViewAttributes: LogViewAttributes): Promise; +} diff --git a/x-pack/plugins/infra/server/types.ts b/x-pack/plugins/infra/server/types.ts index d0fd744b133e3..42c27085380f7 100644 --- a/x-pack/plugins/infra/server/types.ts +++ b/x-pack/plugins/infra/server/types.ts @@ -5,9 +5,29 @@ * 2.0. */ -import type { RequestHandlerContext } from 'src/core/server'; +import type { CoreSetup, RequestHandlerContext } from 'src/core/server'; import type { SearchRequestHandlerContext } from '../../../../src/plugins/data/server'; -import { MlPluginSetup } from '../../ml/server'; +import type { MlPluginSetup } from '../../ml/server'; +import type { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; +import { InfraServerPluginStartDeps } from './lib/adapters/framework'; +import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views/types'; + +export type { InfraConfig } from '../common/plugin_config_types'; + +export type InfraPluginCoreSetup = CoreSetup; +export type InfraPluginStartServicesAccessor = InfraPluginCoreSetup['getStartServices']; + +export interface InfraPluginSetup { + defineInternalSourceConfiguration: ( + sourceId: string, + sourceProperties: InfraStaticSourceConfiguration + ) => void; + logViews: LogViewsServiceSetup; +} + +export interface InfraPluginStart { + logViews: LogViewsServiceStart; +} export type MlSystem = ReturnType; export type MlAnomalyDetectors = ReturnType; @@ -31,24 +51,3 @@ export interface InfraPluginRequestHandlerContext extends RequestHandlerContext infra: InfraRequestHandlerContext; search: SearchRequestHandlerContext; } - -export interface InfraConfig { - alerting: { - inventory_threshold: { - group_by_page_size: number; - }; - metric_threshold: { - group_by_page_size: number; - }; - }; - inventory: { - compositeSize: number; - }; - sources?: { - default?: { - fields?: { - message?: string[]; - }; - }; - }; -} diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index a2d1d2b63655a..b45bd15adb3f8 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -17,6 +17,7 @@ "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/data_views/tsconfig.json" }, { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index b37d88a5dc426..ec964f97922ad 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -35,5 +35,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./file_upload')); loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./watcher')); + loadTestFile(require.resolve('./logs_ui')); }); } diff --git a/x-pack/test/api_integration/apis/logs_ui/index.ts b/x-pack/test/api_integration/apis/logs_ui/index.ts new file mode 100644 index 0000000000000..125ca65f52734 --- /dev/null +++ b/x-pack/test/api_integration/apis/logs_ui/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Logs UI routes', () => { + loadTestFile(require.resolve('./log_views')); + }); +} diff --git a/x-pack/test/api_integration/apis/logs_ui/log_views.ts b/x-pack/test/api_integration/apis/logs_ui/log_views.ts new file mode 100644 index 0000000000000..e2b316218c9fd --- /dev/null +++ b/x-pack/test/api_integration/apis/logs_ui/log_views.ts @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { defaultLogViewId, LogViewAttributes } from '../../../../plugins/infra/common/log_views'; +import { + defaultSourceConfiguration, + infraSourceConfigurationSavedObjectName, + mergeSourceConfiguration, +} from '../../../../plugins/infra/server/lib/sources'; +import { extractSavedObjectReferences } from '../../../../plugins/infra/server/lib/sources/saved_object_references'; +import { logViewSavedObjectName } from '../../../../plugins/infra/server/saved_objects/log_view'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const logViewsService = getService('infraLogViews'); + const kibanaServer = getService('kibanaServer'); + + describe('log view', () => { + describe('GET', () => { + before(async () => { + await kibanaServer.savedObjects.clean({ + types: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName], + }); + }); + + afterEach(async () => { + await kibanaServer.savedObjects.clean({ + types: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName], + }); + }); + + it('falls back to the static infra source default', async () => { + const logView = await logViewsService.getLogView('NONEXISTENT_LOG_VIEW'); + expect(logView.data.origin).to.eql('infra-source-fallback'); + }); + + it('falls back to a stored infra source', async () => { + await kibanaServer.savedObjects.create({ + id: 'default', + type: infraSourceConfigurationSavedObjectName, + overwrite: true, + ...extractSavedObjectReferences( + mergeSourceConfiguration(defaultSourceConfiguration, { + name: 'Test Infra Source', + logIndices: { type: 'index_pattern', indexPatternId: 'NONEXISTENT_INDEX_PATTERN' }, + }) + ), + }); + const logView = await logViewsService.getLogView('default'); + expect(logView.data.origin).to.eql('infra-source-stored'); + expect(logView.data.attributes.name).to.eql('Test Infra Source'); + expect(logView.data.attributes.logIndices).to.eql({ + type: 'data_view', + dataViewId: 'NONEXISTENT_INDEX_PATTERN', + }); + }); + }); + + describe('PUT', () => { + before(async () => { + await kibanaServer.savedObjects.clean({ + types: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName], + }); + }); + + afterEach(async () => { + await kibanaServer.savedObjects.clean({ + types: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName], + }); + }); + + it('stores new log views', async () => { + const logViewAttributes: Partial = { + name: 'Test Log View 1', + description: 'Test Description 1', + logIndices: { type: 'data_view', dataViewId: 'NONEXISTENT_DATA_VIEW' }, + logColumns: [], + }; + + const storedLogView = await logViewsService.putLogView('TEST_LOG_VIEW_1', { + attributes: logViewAttributes, + }); + + expect(storedLogView.data.attributes).to.eql(logViewAttributes); + + const fetchedLogView = await logViewsService.getLogView('TEST_LOG_VIEW_1'); + + expect(fetchedLogView.data.attributes).to.eql(logViewAttributes); + }); + + it('stores new partial log views with default attributes', async () => { + const storedLogView = await logViewsService.putLogView('TEST_LOG_VIEW_1', { + attributes: {}, + }); + + expect(storedLogView.data.attributes.name).to.be.a('string'); + expect(storedLogView.data.attributes.description).to.be.a('string'); + expect(storedLogView.data.attributes.logIndices.type).to.be.a('string'); + expect(storedLogView.data.attributes.logColumns).to.be.an('array'); + expect(storedLogView.data.attributes.logColumns).to.not.be.empty(); + + const fetchedLogView = await logViewsService.getLogView('TEST_LOG_VIEW_1'); + + expect(fetchedLogView.data.attributes.name).to.be.a('string'); + expect(fetchedLogView.data.attributes.description).to.be.a('string'); + expect(fetchedLogView.data.attributes.logIndices.type).to.be.a('string'); + expect(fetchedLogView.data.attributes.logColumns).to.be.an('array'); + expect(fetchedLogView.data.attributes.logColumns).to.not.be.empty(); + }); + + it('overwrites existing log views', async () => { + const initialLogViewAttributes: Partial = { + name: 'Test Log View 1', + description: 'Test Description 1', + logIndices: { type: 'data_view', dataViewId: 'NONEXISTENT_DATA_VIEW' }, + logColumns: [], + }; + const changedLogViewAttributes: Partial = { + name: 'Test Log View 1A', + description: 'Test Description 1A', + logIndices: { type: 'data_view', dataViewId: 'NONEXISTENT_DATA_VIEW_A' }, + logColumns: [{ timestampColumn: { id: 'TIMESTAMP_COLUMN' } }], + }; + + const initialStoredLogView = await logViewsService.putLogView('TEST_LOG_VIEW_1', { + attributes: initialLogViewAttributes, + }); + + expect(initialStoredLogView.data.attributes).to.eql(initialLogViewAttributes); + + const changedStoredLogView = await logViewsService.putLogView('TEST_LOG_VIEW_1', { + attributes: changedLogViewAttributes, + }); + + expect(changedStoredLogView.data.attributes).to.eql(changedLogViewAttributes); + }); + + it('overwrites existing default log view', async () => { + const oldestLogViewAttributes: Partial = { + name: 'Oldest Log View 1', + description: 'Oldest Description 1', + logIndices: { type: 'data_view', dataViewId: 'NONEXISTENT_DATA_VIEW' }, + logColumns: [], + }; + const newerLogViewAttributes: Partial = { + name: 'Newer Log View 1', + description: 'Newer Description 1', + logIndices: { type: 'data_view', dataViewId: 'NONEXISTENT_DATA_VIEW' }, + logColumns: [], + }; + const newestLogViewAttributes: Partial = { + name: 'Newest Log View 1A', + description: 'Newest Description 1A', + logIndices: { type: 'data_view', dataViewId: 'NONEXISTENT_DATA_VIEW_A' }, + logColumns: [{ timestampColumn: { id: 'TIMESTAMP_COLUMN' } }], + }; + + // initially this is the default view + const oldestStoredLogView = await logViewsService.putLogView('OLDEST_LOG_VIEW_ID', { + attributes: oldestLogViewAttributes, + }); + + // check that it's interpreted as the default view + const fetchedOldestLogView = await logViewsService.getLogView(defaultLogViewId); + + expect(oldestStoredLogView).to.eql(fetchedOldestLogView); + + // this becomes the default view now + const newerStoredLogView = await logViewsService.putLogView('NEWER_LOG_VIEW_ID', { + attributes: newerLogViewAttributes, + }); + + expect(newerStoredLogView.data.attributes).to.eql(newerLogViewAttributes); + + // this update should change the newer view + const newestStoredLogView = await logViewsService.putLogView(defaultLogViewId, { + attributes: newestLogViewAttributes, + }); + + expect(newestStoredLogView.data.attributes).to.eql(newestLogViewAttributes); + + // check that default id translation works + expect(newerStoredLogView.data.id).to.eql(newestStoredLogView.data.id); + + // check that the oldest view is unchanged + const refetchedOldestLogView = await logViewsService.getLogView('OLDEST_LOG_VIEW_ID'); + expect(refetchedOldestLogView).to.eql(fetchedOldestLogView); + + // check that the newer view has been changed + const refetchedNewerLogView = await logViewsService.getLogView('NEWER_LOG_VIEW_ID'); + expect(refetchedNewerLogView).to.eql(newestStoredLogView); + }); + }); + }); +} 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 77560d966350e..150a123121051 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/index.js +++ b/x-pack/test/api_integration/apis/metrics_ui/index.js @@ -9,7 +9,6 @@ export default function ({ loadTestFile }) { describe('MetricsUI Endpoints', () => { loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./log_entry_highlights')); - loadTestFile(require.resolve('./log_sources')); loadTestFile(require.resolve('./log_summary')); loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./sources')); diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts b/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts deleted file mode 100644 index 516c262429299..0000000000000 --- a/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts +++ /dev/null @@ -1,183 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { beforeEach } from 'mocha'; -import { - getLogSourceConfigurationSuccessResponsePayloadRT, - patchLogSourceConfigurationSuccessResponsePayloadRT, -} from '../../../../plugins/infra/common/http_api/log_sources'; -import { decodeOrThrow } from '../../../../plugins/infra/common/runtime_types'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const logSourceConfiguration = getService('infraLogSourceConfiguration'); - - describe('log sources api', () => { - before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs')); - beforeEach(() => esArchiver.load('x-pack/test/functional/es_archives/empty_kibana')); - afterEach(() => esArchiver.unload('x-pack/test/functional/es_archives/empty_kibana')); - - describe('source configuration get method for non-existant source', () => { - it('returns the default source configuration', async () => { - const response = await logSourceConfiguration - .createGetLogSourceConfigurationAgent('default') - .expect(200); - - const { - data: { configuration, origin }, - } = decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body); - - expect(origin).to.be('fallback'); - expect(configuration.name).to.be('Default'); - expect(configuration.logIndices).to.eql({ - type: 'index_name', - indexName: 'logs-*,filebeat-*,kibana_sample_data_logs*', - }); - expect(configuration.logColumns[0]).to.have.key('timestampColumn'); - expect(configuration.logColumns[1]).to.have.key('fieldColumn'); - expect(configuration.logColumns[2]).to.have.key('messageColumn'); - }); - }); - - describe('source configuration patch method for non-existant source', () => { - it('creates a source configuration', async () => { - const response = await logSourceConfiguration - .createUpdateLogSourceConfigurationAgent('default', { - name: 'NAME', - description: 'DESCRIPTION', - logIndices: { - type: 'index_pattern', - indexPatternId: 'kip-id', - }, - logColumns: [ - { - messageColumn: { - id: 'MESSAGE_COLUMN', - }, - }, - ], - }) - .expect(200); - - // check direct response - const { - data: { configuration, origin }, - } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); - - expect(configuration.name).to.be('NAME'); - expect(origin).to.be('stored'); - expect(configuration.logIndices).to.eql({ - type: 'index_pattern', - indexPatternId: 'kip-id', - }); - expect(configuration.logColumns).to.have.length(1); - expect(configuration.logColumns[0]).to.have.key('messageColumn'); - - // check for persistence - const { - data: { configuration: persistedConfiguration }, - } = await logSourceConfiguration.getLogSourceConfiguration('default'); - - expect(configuration).to.eql(persistedConfiguration); - }); - - it('creates a source configuration with default values for unspecified properties', async () => { - const response = await logSourceConfiguration - .createUpdateLogSourceConfigurationAgent('default', {}) - .expect(200); - - const { - data: { configuration, origin }, - } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); - - expect(configuration.name).to.be('Default'); - expect(origin).to.be('stored'); - expect(configuration.logIndices).eql({ - type: 'index_name', - indexName: 'logs-*,filebeat-*,kibana_sample_data_logs*', - }); - expect(configuration.logColumns).to.have.length(3); - expect(configuration.logColumns[0]).to.have.key('timestampColumn'); - expect(configuration.logColumns[1]).to.have.key('fieldColumn'); - expect(configuration.logColumns[2]).to.have.key('messageColumn'); - - // check for persistence - const { - data: { configuration: persistedConfiguration, origin: persistedOrigin }, - } = await logSourceConfiguration.getLogSourceConfiguration('default'); - - expect(persistedOrigin).to.be('stored'); - expect(configuration).to.eql(persistedConfiguration); - }); - }); - - describe('source configuration patch method for existing source', () => { - beforeEach(async () => { - await logSourceConfiguration.updateLogSourceConfiguration('default', {}); - }); - - it('updates a source configuration', async () => { - const response = await logSourceConfiguration - .createUpdateLogSourceConfigurationAgent('default', { - name: 'NAME', - description: 'DESCRIPTION', - logIndices: { - type: 'index_pattern', - indexPatternId: 'kip-id', - }, - logColumns: [ - { - messageColumn: { - id: 'MESSAGE_COLUMN', - }, - }, - ], - }) - .expect(200); - - const { - data: { configuration, origin }, - } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); - - expect(configuration.name).to.be('NAME'); - expect(origin).to.be('stored'); - expect(configuration.logIndices).to.eql({ - type: 'index_pattern', - indexPatternId: 'kip-id', - }); - expect(configuration.logColumns).to.have.length(1); - expect(configuration.logColumns[0]).to.have.key('messageColumn'); - }); - - it('partially updates a source configuration', async () => { - const response = await logSourceConfiguration - .createUpdateLogSourceConfigurationAgent('default', { - name: 'NAME', - }) - .expect(200); - - const { - data: { configuration, origin }, - } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); - - expect(configuration.name).to.be('NAME'); - expect(origin).to.be('stored'); - expect(configuration.logIndices).to.eql({ - type: 'index_name', - indexName: 'logs-*,filebeat-*,kibana_sample_data_logs*', - }); - expect(configuration.logColumns).to.have.length(3); - expect(configuration.logColumns[0]).to.have.key('timestampColumn'); - expect(configuration.logColumns[1]).to.have.key('fieldColumn'); - expect(configuration.logColumns[2]).to.have.key('messageColumn'); - }); - }); - }); -} diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index cf439eb7cd5a8..5db28d64cf953 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -16,7 +16,6 @@ import { SupertestWithoutAuthProvider } from './supertest_without_auth'; import { UsageAPIProvider } from './usage_api'; import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration'; -import { InfraLogSourceConfigurationProvider } from './infra_log_source_configuration'; import { MachineLearningProvider } from './ml'; import { IngestManagerProvider } from '../../common/services/ingest_manager'; import { TransformProvider } from './transform'; @@ -29,7 +28,6 @@ export const services = { esSupertestWithoutAuth: EsSupertestWithoutAuthProvider, infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider, - infraLogSourceConfiguration: InfraLogSourceConfigurationProvider, supertestWithoutAuth: SupertestWithoutAuthProvider, usageAPI: UsageAPIProvider, ml: MachineLearningProvider, diff --git a/x-pack/test/api_integration/services/infra_log_source_configuration.ts b/x-pack/test/api_integration/services/infra_log_source_configuration.ts deleted file mode 100644 index cc8eaa81a9e7f..0000000000000 --- a/x-pack/test/api_integration/services/infra_log_source_configuration.ts +++ /dev/null @@ -1,70 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - getLogSourceConfigurationPath, - getLogSourceConfigurationSuccessResponsePayloadRT, - PatchLogSourceConfigurationRequestBody, - patchLogSourceConfigurationRequestBodyRT, - patchLogSourceConfigurationResponsePayloadRT, -} from '../../../plugins/infra/common/http_api/log_sources'; -import { decodeOrThrow } from '../../../plugins/infra/common/runtime_types'; -import { FtrProviderContext } from '../ftr_provider_context'; - -export function InfraLogSourceConfigurationProvider({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const log = getService('log'); - - const createGetLogSourceConfigurationAgent = (sourceId: string) => - supertest - .get(getLogSourceConfigurationPath(sourceId)) - .set({ - 'kbn-xsrf': 'some-xsrf-token', - }) - .send(); - - const getLogSourceConfiguration = async (sourceId: string) => { - log.debug(`Fetching Logs UI source configuration "${sourceId}"`); - - const response = await createGetLogSourceConfigurationAgent(sourceId); - - return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body); - }; - - const createUpdateLogSourceConfigurationAgent = ( - sourceId: string, - sourceProperties: PatchLogSourceConfigurationRequestBody['data'] - ) => - supertest - .patch(getLogSourceConfigurationPath(sourceId)) - .set({ - 'kbn-xsrf': 'some-xsrf-token', - }) - .send(patchLogSourceConfigurationRequestBodyRT.encode({ data: sourceProperties })); - - const updateLogSourceConfiguration = async ( - sourceId: string, - sourceProperties: PatchLogSourceConfigurationRequestBody['data'] - ) => { - log.debug( - `Updating Logs UI source configuration "${sourceId}" with properties ${JSON.stringify( - sourceProperties - )}` - ); - - const response = await createUpdateLogSourceConfigurationAgent(sourceId, sourceProperties); - - return decodeOrThrow(patchLogSourceConfigurationResponsePayloadRT)(response.body); - }; - - return { - createGetLogSourceConfigurationAgent, - createUpdateLogSourceConfigurationAgent, - getLogSourceConfiguration, - updateLogSourceConfiguration, - }; -} diff --git a/x-pack/test/common/services/index.ts b/x-pack/test/common/services/index.ts index b015e10309efb..c51fe7a06e6ac 100644 --- a/x-pack/test/common/services/index.ts +++ b/x-pack/test/common/services/index.ts @@ -5,16 +5,16 @@ * 2.0. */ -import { services as kibanaCommonServices } from '../../../../test/common/services'; import { services as kibanaApiIntegrationServices } from '../../../../test/api_integration/services'; - +import { services as kibanaCommonServices } from '../../../../test/common/services'; +import { InfraLogViewsServiceProvider } from './infra_log_views'; import { SpacesServiceProvider } from './spaces'; import { BSecureSearchProvider } from './bsearch_secure'; export const services = { ...kibanaCommonServices, + infraLogViews: InfraLogViewsServiceProvider, supertest: kibanaApiIntegrationServices.supertest, - spaces: SpacesServiceProvider, secureBsearch: BSecureSearchProvider, }; diff --git a/x-pack/test/common/services/infra_log_views.ts b/x-pack/test/common/services/infra_log_views.ts new file mode 100644 index 0000000000000..df7eb96ebc793 --- /dev/null +++ b/x-pack/test/common/services/infra_log_views.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getLogViewResponsePayloadRT, + getLogViewUrl, + PutLogViewRequestPayload, + putLogViewRequestPayloadRT, + putLogViewResponsePayloadRT, +} from '../../../plugins/infra/common/http_api/log_views'; +import { decodeOrThrow } from '../../../plugins/infra/common/runtime_types'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function InfraLogViewsServiceProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + + const createGetLogViewAgent = (logViewId: string) => + supertest + .get(getLogViewUrl(logViewId)) + .set({ + 'kbn-xsrf': 'some-xsrf-token', + }) + .send(); + + const getLogView = async (logViewId: string) => { + log.debug(`Fetching log view "${logViewId}"...`); + + const response = await createGetLogViewAgent(logViewId); + + return decodeOrThrow(getLogViewResponsePayloadRT)(response.body); + }; + + const createPutLogViewAgent = (logViewId: string, payload: PutLogViewRequestPayload) => + supertest + .put(getLogViewUrl(logViewId)) + .set({ + 'kbn-xsrf': 'some-xsrf-token', + }) + .send(putLogViewRequestPayloadRT.encode(payload)); + + const putLogView = async (logViewId: string, payload: PutLogViewRequestPayload) => { + log.debug(`Storing log view "${logViewId}"...`); + + const response = await createPutLogViewAgent(logViewId, payload); + + return decodeOrThrow(putLogViewResponsePayloadRT)(response.body); + }; + + return { + getLogView, + putLogView, + }; +}