From 8977b56235b3457c38b561133b5b8206ac50f11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 14 Feb 2025 15:55:17 +0100 Subject: [PATCH] [Logs UI] Allow editing of non-resolving log views (#210633) This changes the settings page of the Logs UI such that it allows editing of log view settings even if the resolution or status check failed. This allows recovery from various situations that were previously only recoverable by resetting the log view completely. (cherry picked from commit 41cd657811bd1c00ac15860268c018d4d80085ac) --- .../shared/logs_shared/common/index.ts | 1 + .../logs_shared/common/log_views/errors.ts | 3 + .../common/log_views/resolved_log_view.ts | 14 +++- .../logs_shared/common/log_views/types.ts | 30 +++++--- .../logs_shared/public/hooks/use_log_view.ts | 19 ++++- .../log_view_state/src/state_machine.ts | 17 +++- .../log_view_state/src/types.ts | 12 ++- .../services/log_views/log_views_client.ts | 35 ++++++--- .../indices_configuration_panel.stories.tsx | 8 +- .../settings/indices_configuration_panel.tsx | 47 ++++++++++- .../source_configuration_settings.tsx | 77 ++++++++++++++----- .../utils/logs_overview_fetches.test.ts | 2 +- 12 files changed, 211 insertions(+), 54 deletions(-) diff --git a/x-pack/platform/plugins/shared/logs_shared/common/index.ts b/x-pack/platform/plugins/shared/logs_shared/common/index.ts index e318da11f6b3a..887fe905eb909 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/index.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/index.ts @@ -41,6 +41,7 @@ export { FetchLogViewError, FetchLogViewStatusError, ResolveLogViewError, + isNoSuchRemoteClusterError, } from './log_views/errors'; export type { diff --git a/x-pack/platform/plugins/shared/logs_shared/common/log_views/errors.ts b/x-pack/platform/plugins/shared/logs_shared/common/log_views/errors.ts index 67e5df22406de..6c2bdcee3ad0c 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/log_views/errors.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/log_views/errors.ts @@ -38,3 +38,6 @@ export class PutLogViewError extends Error { this.name = 'PutLogViewError'; } } + +export const isNoSuchRemoteClusterError = (err: Error) => + err?.message?.includes('no_such_remote_cluster_exception'); diff --git a/x-pack/platform/plugins/shared/logs_shared/common/log_views/resolved_log_view.ts b/x-pack/platform/plugins/shared/logs_shared/common/log_views/resolved_log_view.ts index cd2354994db2c..50b751bf40fec 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/log_views/resolved_log_view.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/log_views/resolved_log_view.ts @@ -60,11 +60,16 @@ const resolveLegacyReference = async ( } const indices = logViewAttributes.logIndices.indexName; + const dataViewId = `log-view-${logViewId}`; + + // If we didn't remove the item from the cache here the subsequent call to + // create would not have any effect + dataViewsService.clearInstanceCache(dataViewId); const dataViewReference = await dataViewsService .create( { - id: `log-view-${logViewId}`, + id: dataViewId, name: logViewAttributes.name, title: indices, timeFieldName: TIMESTAMP_FIELD, @@ -134,11 +139,16 @@ const resolveKibanaAdvancedSettingReference = async ( const indices = (await logSourcesService.getLogSources()) .map((logSource) => logSource.indexPattern) .join(','); + const dataViewId = `log-view-${logViewId}`; + + // If we didn't remove the item from the cache here the subsequent call to + // create would not have any effect + dataViewsService.clearInstanceCache(dataViewId); const dataViewReference = await dataViewsService .create( { - id: `log-view-${logViewId}`, + id: dataViewId, name: logViewAttributes.name, title: indices, timeFieldName: TIMESTAMP_FIELD, diff --git a/x-pack/platform/plugins/shared/logs_shared/common/log_views/types.ts b/x-pack/platform/plugins/shared/logs_shared/common/log_views/types.ts index 6cc8d60191a34..b03bca8915b43 100644 --- a/x-pack/platform/plugins/shared/logs_shared/common/log_views/types.ts +++ b/x-pack/platform/plugins/shared/logs_shared/common/log_views/types.ts @@ -106,17 +106,25 @@ export const logViewRT = rt.exact( ); 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 const logViewStatusRT = rt.union([ + rt.strict({ + index: rt.literal('available'), + }), + rt.strict({ + index: rt.literal('empty'), + }), + rt.strict({ + index: rt.literal('missing'), + reason: rt.keyof({ + noIndicesFound: null, + noShardsFound: null, + remoteClusterNotFound: null, + }), + }), + rt.strict({ + index: rt.literal('unknown'), + }), +]); export type LogViewStatus = rt.TypeOf; export const persistedLogViewReferenceRT = rt.type({ diff --git a/x-pack/platform/plugins/shared/logs_shared/public/hooks/use_log_view.ts b/x-pack/platform/plugins/shared/logs_shared/public/hooks/use_log_view.ts index 2c2f640d45ac1..61915b2dadea7 100644 --- a/x-pack/platform/plugins/shared/logs_shared/public/hooks/use_log_view.ts +++ b/x-pack/platform/plugins/shared/logs_shared/public/hooks/use_log_view.ts @@ -9,7 +9,12 @@ import { useInterpret, useSelector } from '@xstate/react'; import createContainer from 'constate'; import { useCallback, useState } from 'react'; import { waitFor } from 'xstate/lib/waitFor'; -import { DEFAULT_LOG_VIEW, LogViewAttributes, LogViewReference } from '../../common/log_views'; +import { + DEFAULT_LOG_VIEW, + LogViewAttributes, + LogViewReference, + LogViewStatus, +} from '../../common/log_views'; import { InitializeFromUrl, UpdateContextInUrl, @@ -73,7 +78,10 @@ export const useLogView = ({ const logView = useSelector(logViewStateService, (state) => state.matches('resolving') || + state.matches('updating') || state.matches('checkingStatus') || + state.matches('resolutionFailed') || + state.matches('checkingStatusFailed') || state.matches('resolvedPersistedLogView') || state.matches('resolvedInlineLogView') ? state.context.logView @@ -82,16 +90,19 @@ export const useLogView = ({ const resolvedLogView = useSelector(logViewStateService, (state) => state.matches('checkingStatus') || + state.matches('checkingStatusFailed') || state.matches('resolvedPersistedLogView') || state.matches('resolvedInlineLogView') ? state.context.resolvedLogView : undefined ); - const logViewStatus = useSelector(logViewStateService, (state) => - state.matches('resolvedPersistedLogView') || state.matches('resolvedInlineLogView') + const logViewStatus: LogViewStatus = useSelector(logViewStateService, (state) => + state.matches('resolvedPersistedLogView') || + state.matches('resolvedInlineLogView') || + state.matches('resolutionFailed') ? state.context.status - : undefined + : { index: 'unknown' } ); const isLoadingLogView = useSelector(logViewStateService, (state) => state.matches('loading')); diff --git a/x-pack/platform/plugins/shared/logs_shared/public/observability_logs/log_view_state/src/state_machine.ts b/x-pack/platform/plugins/shared/logs_shared/public/observability_logs/log_view_state/src/state_machine.ts index 18ba8e4e8a448..f2abde5c78cb7 100644 --- a/x-pack/platform/plugins/shared/logs_shared/public/observability_logs/log_view_state/src/state_machine.ts +++ b/x-pack/platform/plugins/shared/logs_shared/public/observability_logs/log_view_state/src/state_machine.ts @@ -7,6 +7,7 @@ import { catchError, from, map, of, throwError } from 'rxjs'; import { createMachine, actions, assign } from 'xstate'; +import { isNoSuchRemoteClusterError } from '../../../../common'; import { ILogViewsClient } from '../../../services/log_views'; import { NotificationChannel } from '../../xstate_helpers'; import { LogViewNotificationEvent, logViewNotificationEventSelectors } from './notifications'; @@ -75,7 +76,7 @@ export const createPureLogViewStateMachine = (initialContext: LogViewContextWith on: { RESOLUTION_FAILED: { target: 'resolutionFailed', - actions: 'storeError', + actions: ['storeError', 'storeStatusAfterError'], }, RESOLUTION_SUCCEEDED: { target: 'checkingStatus', @@ -250,6 +251,20 @@ export const createPureLogViewStateMachine = (initialContext: LogViewContextWith } as LogViewContextWithStatus) : {} ), + storeStatusAfterError: assign((context, event) => + 'error' in event + ? ({ + status: isNoSuchRemoteClusterError(event.error) + ? { + index: 'missing', + reason: 'remoteClusterNotFound', + } + : { + index: 'unknown', + }, + } as LogViewContextWithStatus) + : {} + ), storeError: assign((context, event) => 'error' in event ? ({ diff --git a/x-pack/platform/plugins/shared/logs_shared/public/observability_logs/log_view_state/src/types.ts b/x-pack/platform/plugins/shared/logs_shared/public/observability_logs/log_view_state/src/types.ts index 3a7914e8ff6dc..8174b41aaa1dc 100644 --- a/x-pack/platform/plugins/shared/logs_shared/public/observability_logs/log_view_state/src/types.ts +++ b/x-pack/platform/plugins/shared/logs_shared/public/observability_logs/log_view_state/src/types.ts @@ -71,7 +71,7 @@ export type LogViewTypestate = } | { value: 'updating'; - context: LogViewContextWithReference; + context: LogViewContextWithReference & LogViewContextWithLogView; } | { value: 'loadingFailed'; @@ -83,11 +83,17 @@ export type LogViewTypestate = } | { value: 'resolutionFailed'; - context: LogViewContextWithReference & LogViewContextWithLogView & LogViewContextWithError; + context: LogViewContextWithReference & + LogViewContextWithLogView & + LogViewContextWithStatus & + LogViewContextWithError; } | { value: 'checkingStatusFailed'; - context: LogViewContextWithReference & LogViewContextWithLogView & LogViewContextWithError; + context: LogViewContextWithReference & + LogViewContextWithLogView & + LogViewContextWithResolvedLogView & + LogViewContextWithError; }; export type LogViewContext = LogViewTypestate['context']; diff --git a/x-pack/platform/plugins/shared/logs_shared/public/services/log_views/log_views_client.ts b/x-pack/platform/plugins/shared/logs_shared/public/services/log_views/log_views_client.ts index b1a71cea73cb1..7fc34a1035114 100644 --- a/x-pack/platform/plugins/shared/logs_shared/public/services/log_views/log_views_client.ts +++ b/x-pack/platform/plugins/shared/logs_shared/public/services/log_views/log_views_client.ts @@ -25,6 +25,7 @@ import { PutLogViewError, ResolvedLogView, resolveLogView, + isNoSuchRemoteClusterError, } from '../../../common/log_views'; import { decodeOrThrow } from '../../../common/runtime_types'; import { ILogViewsClient } from './types'; @@ -69,7 +70,7 @@ export class LogViewsClient implements ILogViewsClient { } public async getResolvedLogViewStatus(resolvedLogView: ResolvedLogView): Promise { - const indexStatus = await lastValueFrom( + return await lastValueFrom( this.search({ params: { ignore_unavailable: true, @@ -81,31 +82,43 @@ export class LogViewsClient implements ILogViewsClient { }, }) ).then( - ({ rawResponse }) => { + ({ rawResponse }): LogViewStatus => { if (rawResponse._shards.total <= 0) { - return 'missing' as const; + return { + index: 'missing', + reason: 'noShardsFound', + }; } const totalHits = decodeTotalHits(rawResponse.hits.total); if (typeof totalHits === 'number' ? totalHits > 0 : totalHits.value > 0) { - return 'available' as const; + return { + index: 'available', + }; } - return 'empty' as const; + return { + index: 'empty', + }; }, - (err) => { + (err): LogViewStatus => { if (err.status === 404) { - return 'missing' as const; + return { + index: 'missing', + reason: 'noIndicesFound', + }; + } else if (err != null && isNoSuchRemoteClusterError(err)) { + return { + index: 'missing', + reason: 'remoteClusterNotFound', + }; } + throw new FetchLogViewStatusError( `Failed to check status of log indices of "${resolvedLogView.indices}": ${err}` ); } ); - - return { - index: indexStatus, - }; } public async putLogView( diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx index 864797635312e..d44ee000aaab1 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx @@ -59,7 +59,7 @@ type IndicesConfigurationPanelProps = PropsOf; type IndicesConfigurationPanelStoryArgs = Pick< IndicesConfigurationPanelProps, - 'isLoading' | 'isReadOnly' + 'isLoading' | 'isReadOnly' | 'logViewStatus' > & { availableIndexPatterns: MockIndexPatternSpec[]; logIndices: LogIndicesFormState; @@ -69,6 +69,7 @@ const IndicesConfigurationPanelTemplate: Story { const logIndicesFormElement = useLogIndicesFormElement(logIndices); @@ -78,6 +79,7 @@ const IndicesConfigurationPanelTemplate: Story // field states{'\n'} @@ -103,6 +105,10 @@ const defaultArgs: IndicesConfigurationPanelStoryArgs = { type: 'index_name' as const, indexName: 'logs-*', }, + logViewStatus: { + index: 'missing', + reason: 'remoteClusterNotFound', + }, availableIndexPatterns: [ { id: 'INDEX_PATTERN_A', diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx index 70c59259a2c52..bb1bf7b111c35 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx @@ -9,7 +9,11 @@ import { EuiCheckableCard, EuiFormFieldset, EuiSpacer, EuiTitle } from '@elastic import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useEffect, useState } from 'react'; import { useUiTracker } from '@kbn/observability-shared-plugin/public'; -import type { LogDataViewReference, LogIndexReference } from '@kbn/logs-shared-plugin/common'; +import type { + LogDataViewReference, + LogIndexReference, + LogViewStatus, +} from '@kbn/logs-shared-plugin/common'; import { logIndexNameReferenceRT, logDataViewReferenceRT, @@ -35,7 +39,8 @@ export const IndicesConfigurationPanel = React.memo<{ isLoading: boolean; isReadOnly: boolean; indicesFormElement: FormElement; -}>(({ isLoading, isReadOnly, indicesFormElement }) => { + logViewStatus: LogViewStatus; +}>(({ isLoading, isReadOnly, indicesFormElement, logViewStatus }) => { const { services: { http, @@ -200,6 +205,7 @@ export const IndicesConfigurationPanel = React.memo<{ /> )} + {numberOfLogsRules > 0 && indicesFormElement.isDirty && ( <> @@ -234,6 +240,36 @@ export const IndicesConfigurationPanel = React.memo<{ ); }); +const LogViewStatusWarning: React.FC<{ logViewStatus: LogViewStatus }> = ({ logViewStatus }) => { + if (logViewStatus.index === 'missing') { + return ( + <> + + + {logViewStatus.reason === 'noShardsFound' ? ( + + ) : logViewStatus.reason === 'noIndicesFound' ? ( + + ) : logViewStatus.reason === 'remoteClusterNotFound' ? ( + + ) : null} + + + ); + } else { + return null; + } +}; + const isDataViewFormElement = isFormElementForType( (value): value is LogDataViewReference | undefined => value == null || logDataViewReferenceRT.is(value) @@ -244,3 +280,10 @@ const isIndexNamesFormElement = isFormElementForType(logIndexNameReferenceRT.is) const isKibanaAdvancedSettingFormElement = isFormElementForType( logSourcesKibanaAdvancedSettingRT.is ); + +const logIndicesMissingTitle = i18n.translate( + 'xpack.infra.sourceConfiguration.logIndicesMissingTitle', + { + defaultMessage: 'Log indices missing', + } +); diff --git a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx index d1df2a5820dd3..d5a75d20ccf5a 100644 --- a/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx +++ b/x-pack/solutions/observability/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -20,6 +20,8 @@ import { useKibana } from '@kbn/kibana-react-plugin/public'; import { Prompt } from '@kbn/observability-shared-plugin/public'; import { useTrackPageview } from '@kbn/observability-shared-plugin/public'; import { useLogViewContext } from '@kbn/logs-shared-plugin/public'; +import type { LogView, LogViewAttributes, LogViewStatus } from '@kbn/logs-shared-plugin/common'; +import { SourceErrorPage } from '../../../components/source_error_page'; import { LogsDeprecationCallout } from '../../../components/logs_deprecation_callout'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useLogsBreadcrumbs } from '../../../hooks/use_logs_breadcrumbs'; @@ -50,14 +52,17 @@ export const LogsSettingsPage = () => { ]); const { - logView, hasFailedLoadingLogView, + isInlineLogView, isLoading, isUninitialized, - update, + latestLoadLogViewFailures, + logView, + logViewStatus, resolvedLogView, - isInlineLogView, + retry, revertToDefaultLogView, + update, } = useLogViewContext(); const availableFields = useMemo( @@ -65,6 +70,47 @@ export const LogsSettingsPage = () => { [resolvedLogView] ); + const isWriteable = shouldAllowEdit && logView != null && logView.origin !== 'internal'; + + if ((isLoading || isUninitialized) && logView == null) { + return ; + } else if (hasFailedLoadingLogView || logView == null) { + return ; + } else { + return ( + + ); + } +}; + +const LogsSettingsPageContent = ({ + availableFields, + isInlineLogView, + isLoading, + isWriteable, + logView, + logViewStatus, + revertToDefaultLogView, + onUpdateLogViewAttributes, +}: { + availableFields: string[]; + isInlineLogView: boolean; + isLoading: boolean; + isWriteable: boolean; + logView: LogView; + logViewStatus: LogViewStatus; + revertToDefaultLogView: () => void; + onUpdateLogViewAttributes: (logViewAttributes: Partial) => Promise; +}) => { const { sourceConfigurationFormElement, formState, @@ -74,21 +120,15 @@ export const LogsSettingsPage = () => { } = useLogSourceConfigurationFormState(logView?.attributes); const persistUpdates = useCallback(async () => { - await update(formState); - sourceConfigurationFormElement.resetValue(); - }, [update, sourceConfigurationFormElement, formState]); - - const isWriteable = useMemo( - () => shouldAllowEdit && logView && logView.origin !== 'internal', - [shouldAllowEdit, logView] - ); - - if ((isLoading || isUninitialized) && !resolvedLogView) { - return ; - } - if (hasFailedLoadingLogView) { - return null; - } + try { + await onUpdateLogViewAttributes(formState); + sourceConfigurationFormElement.resetValue(); + } catch { + // the error is handled in the state machine already, but without this the + // global promise rejection tracker would complain about it being + // unhandled + } + }, [onUpdateLogViewAttributes, sourceConfigurationFormElement, formState]); return ( @@ -124,6 +164,7 @@ export const LogsSettingsPage = () => { isLoading={isLoading} isReadOnly={!isWriteable} indicesFormElement={logIndicesFormElement} + logViewStatus={logViewStatus} /> diff --git a/x-pack/solutions/observability/plugins/infra/public/utils/logs_overview_fetches.test.ts b/x-pack/solutions/observability/plugins/infra/public/utils/logs_overview_fetches.test.ts index 8b421192129d2..65a9b7d47dfb0 100644 --- a/x-pack/solutions/observability/plugins/infra/public/utils/logs_overview_fetches.test.ts +++ b/x-pack/solutions/observability/plugins/infra/public/utils/logs_overview_fetches.test.ts @@ -107,7 +107,7 @@ describe('Logs UI Observability Homepage Functions', () => { setup(); getResolvedLogView.mockResolvedValue(createResolvedLogViewMock({ indices: 'test-index' })); - getResolvedLogViewStatus.mockResolvedValue({ index: 'missing' }); + getResolvedLogViewStatus.mockResolvedValue({ index: 'missing', reason: 'noShardsFound' }); const hasData = getLogsHasDataFetcher(mockedGetStartServices); const response = await hasData();