Skip to content

Commit

Permalink
[8.18] [Logs UI] Allow editing of non-resolving log views (#210633) (#…
Browse files Browse the repository at this point in the history
…211236)

# Backport

This will backport the following commits from `8.x` to `8.18`:
- [[Logs UI] Allow editing of non-resolving log views
(#210633)](#210633)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Felix
Stürmer","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-02-14T14:55:17Z","message":"[Logs
UI] Allow editing of non-resolving log views (#210633)\n\nThis 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.","sha":"41cd657811bd1c00ac15860268c018d4d80085ac","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Feature:Logs
UI","backport:skip","Team:obs-ux-logs","v8.18.0","v8.17.3"],"title":"[Logs
UI] Allow editing of non-resolving log
views","number":210633,"url":"https://github.com/elastic/kibana/pull/210633","mergeCommit":{"message":"[Logs
UI] Allow editing of non-resolving log views (#210633)\n\nThis 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.","sha":"41cd657811bd1c00ac15860268c018d4d80085ac"}},"sourceBranch":"8.x","suggestedTargetBranches":["8.18","8.17"],"targetPullRequestStates":[{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.17","label":"v8.17.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
  • Loading branch information
weltenwort authored Feb 15, 2025
1 parent 44ebe22 commit 245ef36
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 54 deletions.
1 change: 1 addition & 0 deletions x-pack/platform/plugins/shared/logs_shared/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export {
FetchLogViewError,
FetchLogViewStatusError,
ResolveLogViewError,
isNoSuchRemoteClusterError,
} from './log_views/errors';

export type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,25 @@ export const logViewRT = rt.exact(
);
export type LogView = rt.TypeOf<typeof logViewRT>;

export const logViewIndexStatusRT = rt.keyof({
available: null,
empty: null,
missing: null,
unknown: null,
});
export type LogViewIndexStatus = rt.TypeOf<typeof logViewIndexStatusRT>;

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<typeof logViewStatusRT>;

export const persistedLogViewReferenceRT = rt.type({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -75,7 +76,7 @@ export const createPureLogViewStateMachine = (initialContext: LogViewContextWith
on: {
RESOLUTION_FAILED: {
target: 'resolutionFailed',
actions: 'storeError',
actions: ['storeError', 'storeStatusAfterError'],
},
RESOLUTION_SUCCEEDED: {
target: 'checkingStatus',
Expand Down Expand Up @@ -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
? ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export type LogViewTypestate =
}
| {
value: 'updating';
context: LogViewContextWithReference;
context: LogViewContextWithReference & LogViewContextWithLogView;
}
| {
value: 'loadingFailed';
Expand All @@ -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'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
PutLogViewError,
ResolvedLogView,
resolveLogView,
isNoSuchRemoteClusterError,
} from '../../../common/log_views';
import { decodeOrThrow } from '../../../common/runtime_types';
import { ILogViewsClient } from './types';
Expand Down Expand Up @@ -69,7 +70,7 @@ export class LogViewsClient implements ILogViewsClient {
}

public async getResolvedLogViewStatus(resolvedLogView: ResolvedLogView): Promise<LogViewStatus> {
const indexStatus = await lastValueFrom(
return await lastValueFrom(
this.search({
params: {
ignore_unavailable: true,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type IndicesConfigurationPanelProps = PropsOf<typeof IndicesConfigurationPanel>;

type IndicesConfigurationPanelStoryArgs = Pick<
IndicesConfigurationPanelProps,
'isLoading' | 'isReadOnly'
'isLoading' | 'isReadOnly' | 'logViewStatus'
> & {
availableIndexPatterns: MockIndexPatternSpec[];
logIndices: LogIndicesFormState;
Expand All @@ -69,6 +69,7 @@ const IndicesConfigurationPanelTemplate: Story<IndicesConfigurationPanelStoryArg
isLoading,
isReadOnly,
logIndices,
logViewStatus,
}) => {
const logIndicesFormElement = useLogIndicesFormElement(logIndices);

Expand All @@ -78,6 +79,7 @@ const IndicesConfigurationPanelTemplate: Story<IndicesConfigurationPanelStoryArg
isLoading={isLoading}
isReadOnly={isReadOnly}
indicesFormElement={logIndicesFormElement}
logViewStatus={logViewStatus}
/>
<EuiCodeBlock language="json">
// field states{'\n'}
Expand All @@ -103,6 +105,10 @@ const defaultArgs: IndicesConfigurationPanelStoryArgs = {
type: 'index_name' as const,
indexName: 'logs-*',
},
logViewStatus: {
index: 'missing',
reason: 'remoteClusterNotFound',
},
availableIndexPatterns: [
{
id: 'INDEX_PATTERN_A',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,7 +39,8 @@ export const IndicesConfigurationPanel = React.memo<{
isLoading: boolean;
isReadOnly: boolean;
indicesFormElement: FormElement<LogIndexReference | undefined, FormValidationError>;
}>(({ isLoading, isReadOnly, indicesFormElement }) => {
logViewStatus: LogViewStatus;
}>(({ isLoading, isReadOnly, indicesFormElement, logViewStatus }) => {
const {
services: {
http,
Expand Down Expand Up @@ -200,6 +205,7 @@ export const IndicesConfigurationPanel = React.memo<{
/>
)}
</EuiCheckableCard>
<LogViewStatusWarning logViewStatus={logViewStatus} />
{numberOfLogsRules > 0 && indicesFormElement.isDirty && (
<>
<EuiSpacer size="s" />
Expand Down Expand Up @@ -234,6 +240,36 @@ export const IndicesConfigurationPanel = React.memo<{
);
});

const LogViewStatusWarning: React.FC<{ logViewStatus: LogViewStatus }> = ({ logViewStatus }) => {
if (logViewStatus.index === 'missing') {
return (
<>
<EuiSpacer size="s" />
<EuiCallOut title={logIndicesMissingTitle} color="warning" iconType="warning">
{logViewStatus.reason === 'noShardsFound' ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.logIndicesMissingMessage.noShardsFound"
defaultMessage="No shards found for the specified indices."
/>
) : logViewStatus.reason === 'noIndicesFound' ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.logIndicesMissingMessage.noIndicesFound"
defaultMessage="No indices found for the specified pattern."
/>
) : logViewStatus.reason === 'remoteClusterNotFound' ? (
<FormattedMessage
id="xpack.infra.sourceConfiguration.logIndicesMissingMessage.remoteClusterNotFound"
defaultMessage="At least one remote cluster was not found."
/>
) : null}
</EuiCallOut>
</>
);
} else {
return null;
}
};

const isDataViewFormElement = isFormElementForType(
(value): value is LogDataViewReference | undefined =>
value == null || logDataViewReferenceRT.is(value)
Expand All @@ -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',
}
);
Loading

0 comments on commit 245ef36

Please sign in to comment.