[Logs UI] Prevent broken KIP references from breaking the Logs UI (#98532)

This fixes problems in handling broken KIP references and reduces the risk of broken references occurring the first place.
This commit is contained in:
Felix Stürmer 2021-04-30 16:18:52 +02:00 committed by GitHub
parent 577948bea3
commit 0e948cffc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 809 additions and 280 deletions

View file

@ -0,0 +1,40 @@
/*
* 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.
*/
/* eslint-disable max-classes-per-file */
export class ResolveLogSourceConfigurationError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'ResolveLogSourceConfigurationError';
}
}
export class FetchLogSourceConfigurationError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'FetchLogSourceConfigurationError';
}
}
export class FetchLogSourceStatusError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'FetchLogSourceStatusError';
}
}
export class PatchLogSourceConfigurationError extends Error {
constructor(message: string, public cause?: Error) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'PatchLogSourceConfigurationError';
}
}

View file

@ -5,5 +5,6 @@
* 2.0.
*/
export * from './errors';
export * from './log_source_configuration';
export * from './resolved_log_source_configuration';

View file

@ -8,6 +8,7 @@
import { estypes } from '@elastic/elasticsearch';
import { IndexPattern, IndexPatternsContract } from '../../../../../src/plugins/data/common';
import { ObjectEntries } from '../utility_types';
import { ResolveLogSourceConfigurationError } from './errors';
import {
LogSourceColumnConfiguration,
LogSourceConfigurationProperties,
@ -44,10 +45,19 @@ const resolveLegacyReference = async (
throw new Error('This function can only resolve legacy references');
}
const fields = await indexPatternsService.getFieldsForWildcard({
pattern: sourceConfiguration.logIndices.indexName,
allowNoIndex: true,
});
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,
@ -70,9 +80,14 @@ const resolveKibanaIndexPatternReference = async (
throw new Error('This function can only resolve Kibana Index Pattern references');
}
const indexPattern = await indexPatternsService.get(
sourceConfiguration.logIndices.indexPatternId
);
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,

View file

@ -160,12 +160,6 @@ export const SavedSourceConfigurationRuntimeType = rt.intersection([
export interface InfraSavedSourceConfiguration
extends rt.TypeOf<typeof SavedSourceConfigurationRuntimeType> {}
export const pickSavedSourceConfiguration = (
value: InfraSourceConfiguration
): InfraSavedSourceConfiguration => {
return value;
};
/**
* Static source configuration, the result of merging values from the config file and
* hardcoded defaults.

View file

@ -13,10 +13,10 @@ import {
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { euiStyled } from '../../../../../src/plugins/kibana_react/common';
import { FlexPage } from './page';
@ -45,7 +45,7 @@ export const ErrorPage: React.FC<Props> = ({ detailedMessage, retry, shortMessag
/>
}
>
<EuiFlexGroup>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>{shortMessage}</EuiFlexItem>
{retry ? (
<EuiFlexItem grow={false}>
@ -58,7 +58,12 @@ export const ErrorPage: React.FC<Props> = ({ detailedMessage, retry, shortMessag
</EuiFlexItem>
) : null}
</EuiFlexGroup>
{detailedMessage ? <div>{detailedMessage}</div> : null}
{detailedMessage ? (
<>
<EuiSpacer />
<div>{detailedMessage}</div>
</>
) : null}
</EuiCallOut>
</EuiPageContentBody>
</MinimumPageContent>

View file

@ -111,10 +111,10 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
}
const {
sourceConfiguration,
loadSourceConfiguration,
isLoadingSourceConfiguration,
derivedIndexPattern,
isLoadingSourceConfiguration,
loadSource,
sourceConfiguration,
} = useLogSource({
sourceId,
fetch: services.http.fetch,
@ -164,8 +164,8 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re
// Component lifetime
useEffect(() => {
loadSourceConfiguration();
}, [loadSourceConfiguration]);
loadSource();
}, [loadSource]);
useEffect(() => {
fetchEntries();

View file

@ -0,0 +1,141 @@
/*
* 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 {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiEmptyPrompt,
EuiPageTemplate,
EuiSpacer,
} from '@elastic/eui';
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 '../../hooks/use_link_props';
export const LogSourceErrorPage: React.FC<{
errors: Error[];
onRetry: () => void;
}> = ({ errors, onRetry }) => {
const settingsLinkProps = useLinkProps({ app: 'logs', pathname: '/settings' });
return (
<EuiPageTemplate template="centeredBody" pageContentProps={{ paddingSize: 'none' }}>
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={
<h2>
<FormattedMessage
id="xpack.infra.logSourceErrorPage.failedToLoadSourceTitle"
defaultMessage="Failed to load configuration"
/>
</h2>
}
body={
<>
<p>
<FormattedMessage
id="xpack.infra.logSourceErrorPage.failedToLoadSourceMessage"
defaultMessage="Errors occurred while attempting to load the configuration. Try again or change the configuration to fix the problem."
/>
</p>
{errors.map((error) => (
<React.Fragment key={error.name}>
<LogSourceErrorMessage error={error} />
<EuiSpacer />
</React.Fragment>
))}
</>
}
actions={[
<EuiButton onClick={onRetry} iconType="refresh" fill>
<FormattedMessage
id="xpack.infra.logSourceErrorPage.tryAgainButtonLabel"
defaultMessage="Try again"
/>
</EuiButton>,
<EuiButtonEmpty iconType="gear" {...settingsLinkProps}>
<FormattedMessage
id="xpack.infra.logSourceErrorPage.navigateToSettingsButtonLabel"
defaultMessage="Change configuration"
/>
</EuiButtonEmpty>,
]}
/>
</EuiPageTemplate>
);
};
const LogSourceErrorMessage: React.FC<{ error: Error }> = ({ error }) => {
if (error instanceof ResolveLogSourceConfigurationError) {
return (
<LogSourceErrorCallout
title={
<FormattedMessage
id="xpack.infra.logSourceErrorPage.resolveLogSourceConfigurationErrorTitle"
defaultMessage="Failed to resolve the log source configuration"
/>
}
>
{error.cause instanceof SavedObjectNotFound ? (
// the SavedObjectNotFound error message contains broken markup
<FormattedMessage
id="xpack.infra.logSourceErrorPage.savedObjectNotFoundErrorMessage"
defaultMessage="Failed to locate that {savedObjectType}: {savedObjectId}"
values={{
savedObjectType: error.cause.savedObjectType,
savedObjectId: error.cause.savedObjectId,
}}
/>
) : (
`${error.cause?.message ?? error.message}`
)}
</LogSourceErrorCallout>
);
} else if (error instanceof FetchLogSourceConfigurationError) {
return (
<LogSourceErrorCallout
title={
<FormattedMessage
id="xpack.infra.logSourceErrorPage.fetchLogSourceConfigurationErrorTitle"
defaultMessage="Failed to load the log source configuration"
/>
}
>
{`${error.cause?.message ?? error.message}`}
</LogSourceErrorCallout>
);
} else if (error instanceof FetchLogSourceStatusError) {
return (
<LogSourceErrorCallout
title={
<FormattedMessage
id="xpack.infra.logSourceErrorPage.fetchLogSourceStatusErrorTitle"
defaultMessage="Failed to determine the status of the log source"
/>
}
>
{`${error.cause?.message ?? error.message}`}
</LogSourceErrorCallout>
);
} else {
return <LogSourceErrorCallout title={error.name}>{`${error.message}`}</LogSourceErrorCallout>;
}
};
const LogSourceErrorCallout: React.FC<{ title: React.ReactNode }> = ({ title, children }) => (
<EuiCallOut className="eui-textLeft" color="danger" iconType="alert" title={title}>
<p>{children}</p>
</EuiCallOut>
);

View file

@ -10,12 +10,24 @@ import {
getLogSourceConfigurationPath,
getLogSourceConfigurationSuccessResponsePayloadRT,
} from '../../../../../common/http_api/log_sources';
import { FetchLogSourceConfigurationError } from '../../../../../common/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
export const callFetchLogSourceConfigurationAPI = async (sourceId: string, fetch: HttpHandler) => {
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)(response);
return decodeOrThrow(
getLogSourceConfigurationSuccessResponsePayloadRT,
(message: string) =>
new FetchLogSourceConfigurationError(
`Failed to decode log source configuration "${sourceId}": ${message}`
)
)(response);
};

View file

@ -10,12 +10,24 @@ 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)(response);
return decodeOrThrow(
getLogSourceStatusSuccessResponsePayloadRT,
(message: string) =>
new FetchLogSourceStatusError(
`Failed to decode status for log source "${sourceId}": ${message}`
)
)(response);
};

View file

@ -12,6 +12,7 @@ import {
patchLogSourceConfigurationRequestBodyRT,
LogSourceConfigurationPropertiesPatch,
} from '../../../../../common/http_api/log_sources';
import { PatchLogSourceConfigurationError } from '../../../../../common/log_sources';
import { decodeOrThrow } from '../../../../../common/runtime_types';
export const callPatchLogSourceConfigurationAPI = async (
@ -26,7 +27,18 @@ export const callPatchLogSourceConfigurationAPI = async (
data: patchedProperties,
})
),
}).catch((error) => {
throw new PatchLogSourceConfigurationError(
`Failed to update log source configuration "${sourceId}": ${error}`,
error
);
});
return decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response);
return decodeOrThrow(
patchLogSourceConfigurationSuccessResponsePayloadRT,
(message: string) =>
new PatchLogSourceConfigurationError(
`Failed to decode log source configuration "${sourceId}": ${message}`
)
)(response);
};

View file

@ -18,9 +18,10 @@ export const createUninitializedUseLogSourceMock: CreateUseLogSource = ({
fields: [],
title: 'unknown',
},
hasFailedLoading: false,
hasFailedLoadingSource: false,
hasFailedLoadingSourceStatus: false,
hasFailedResolvingSourceConfiguration: false,
hasFailedResolvingSource: false,
initialize: jest.fn(),
isLoading: false,
isLoadingSourceConfiguration: false,
@ -29,13 +30,13 @@ export const createUninitializedUseLogSourceMock: CreateUseLogSource = ({
isUninitialized: true,
loadSource: jest.fn(),
loadSourceConfiguration: jest.fn(),
loadSourceFailureMessage: undefined,
latestLoadSourceFailures: [],
resolveSourceFailureMessage: undefined,
loadSourceStatus: jest.fn(),
sourceConfiguration: undefined,
sourceId,
sourceStatus: undefined,
updateSourceConfiguration: jest.fn(),
updateSource: jest.fn(),
resolvedSourceConfiguration: undefined,
loadResolveLogSourceConfiguration: jest.fn(),
});
@ -83,6 +84,6 @@ export const createBasicSourceConfiguration = (sourceId: string): LogSourceConfi
},
});
export const createAvailableSourceStatus = (logIndexFields = []): LogSourceStatus => ({
export const createAvailableSourceStatus = (): LogSourceStatus => ({
logIndexStatus: 'available',
});

View file

@ -7,8 +7,8 @@
import createContainer from 'constate';
import { useCallback, useMemo, useState } from 'react';
import useMountedState from 'react-use/lib/useMountedState';
import type { HttpHandler } from 'src/core/public';
import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common';
import {
LogIndexField,
LogSourceConfigurationPropertiesPatch,
@ -19,12 +19,12 @@ import {
LogSourceConfigurationProperties,
ResolvedLogSourceConfiguration,
resolveLogSourceConfiguration,
ResolveLogSourceConfigurationError,
} from '../../../../common/log_sources';
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
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';
import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common';
export {
LogIndexField,
@ -32,6 +32,7 @@ export {
LogSourceConfigurationProperties,
LogSourceConfigurationPropertiesPatch,
LogSourceStatus,
ResolveLogSourceConfigurationError,
};
export const useLogSource = ({
@ -43,7 +44,6 @@ export const useLogSource = ({
fetch: HttpHandler;
indexPatternsService: IndexPatternsContract;
}) => {
const getIsMounted = useMountedState();
const [sourceConfiguration, setSourceConfiguration] = useState<
LogSourceConfiguration | undefined
>(undefined);
@ -58,52 +58,34 @@ export const useLogSource = ({
{
cancelPreviousOn: 'resolution',
createPromise: async () => {
const { data: sourceConfigurationResponse } = await callFetchLogSourceConfigurationAPI(
sourceId,
fetch
);
const resolvedSourceConfigurationResponse = await resolveLogSourceConfiguration(
sourceConfigurationResponse?.configuration,
indexPatternsService
);
return { sourceConfigurationResponse, resolvedSourceConfigurationResponse };
},
onResolve: ({ sourceConfigurationResponse, resolvedSourceConfigurationResponse }) => {
if (!getIsMounted()) {
return;
}
setSourceConfiguration(sourceConfigurationResponse);
setResolvedSourceConfiguration(resolvedSourceConfigurationResponse);
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) => {
const { data: updatedSourceConfig } = await callPatchLogSourceConfigurationAPI(
sourceId,
patchedProperties,
fetch
);
const resolvedSourceConfig = await resolveLogSourceConfiguration(
updatedSourceConfig.configuration,
indexPatternsService
);
return { updatedSourceConfig, resolvedSourceConfig };
},
onResolve: ({ updatedSourceConfig, resolvedSourceConfig }) => {
if (!getIsMounted()) {
return;
}
setSourceConfiguration(updatedSourceConfig);
setResolvedSourceConfiguration(resolvedSourceConfig);
loadSourceStatus();
return (await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties, fetch)).data;
},
onResolve: setSourceConfiguration,
},
[sourceId, fetch, indexPatternsService]
);
@ -114,13 +96,7 @@ export const useLogSource = ({
createPromise: async () => {
return await callFetchLogSourceStatusAPI(sourceId, fetch);
},
onResolve: ({ data }) => {
if (!getIsMounted()) {
return;
}
setSourceStatus(data);
},
onResolve: ({ data }) => setSourceStatus(data),
},
[sourceId, fetch]
);
@ -133,53 +109,67 @@ export const useLogSource = ({
[resolvedSourceConfiguration]
);
const isLoadingSourceConfiguration = useMemo(
() => loadSourceConfigurationRequest.state === 'pending',
[loadSourceConfigurationRequest.state]
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 isUpdatingSourceConfiguration = useMemo(
() => updateSourceConfigurationRequest.state === 'pending',
[updateSourceConfigurationRequest.state]
);
const isLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'pending', [
loadSourceStatusRequest.state,
]);
const isLoading = useMemo(
() => isLoadingSourceConfiguration || isLoadingSourceStatus || isUpdatingSourceConfiguration,
[isLoadingSourceConfiguration, isLoadingSourceStatus, isUpdatingSourceConfiguration]
);
const isUninitialized = useMemo(
() =>
loadSourceConfigurationRequest.state === 'uninitialized' ||
loadSourceStatusRequest.state === 'uninitialized',
[loadSourceConfigurationRequest.state, loadSourceStatusRequest.state]
);
const hasFailedLoadingSource = useMemo(
() => loadSourceConfigurationRequest.state === 'rejected',
[loadSourceConfigurationRequest.state]
);
const hasFailedLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'rejected', [
loadSourceStatusRequest.state,
]);
const loadSourceFailureMessage = useMemo(
() =>
loadSourceConfigurationRequest.state === 'rejected'
? `${loadSourceConfigurationRequest.value}`
: undefined,
[loadSourceConfigurationRequest]
);
const loadSource = useCallback(() => {
return Promise.all([loadSourceConfiguration(), loadSourceStatus()]);
}, [loadSourceConfiguration, loadSourceStatus]);
const initialize = useCallback(async () => {
if (!isUninitialized) {
return;
@ -194,21 +184,23 @@ export const useLogSource = ({
isUninitialized,
derivedIndexPattern,
// Failure states
hasFailedLoading,
hasFailedLoadingSource,
hasFailedLoadingSourceStatus,
loadSourceFailureMessage,
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,
loadSourceConfiguration,
sourceConfiguration,
updateSourceConfiguration,
updateSource,
// Resolved source configuration (represents a fully resolved state, you would use this for the vast majority of "read" scenarios)
resolvedSourceConfiguration,
};

View file

@ -36,7 +36,7 @@ export const RedirectToNodeLogs = ({
location,
}: RedirectToNodeLogsType) => {
const { services } = useKibanaContextForPlugin();
const { isLoading, loadSourceConfiguration, sourceConfiguration } = useLogSource({
const { isLoading, loadSource, sourceConfiguration } = useLogSource({
fetch: services.http.fetch,
sourceId,
indexPatternsService: services.data.indexPatterns,
@ -44,7 +44,7 @@ export const RedirectToNodeLogs = ({
const fields = sourceConfiguration?.configuration.fields;
useMount(() => {
loadSourceConfiguration();
loadSource();
});
if (isLoading) {

View file

@ -7,7 +7,6 @@
import { i18n } from '@kbn/i18n';
import React, { useCallback, useEffect } from 'react';
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
@ -19,23 +18,13 @@ import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogEntryCategoriesResultsContent } from './page_results_content';
import { LogEntryCategoriesSetupContent } from './page_setup_content';
export const LogEntryCategoriesPageContent = () => {
const {
hasFailedLoadingSource,
isLoading,
isUninitialized,
loadSource,
loadSourceFailureMessage,
} = useLogSourceContext();
const {
hasLogAnalysisCapabilites,
hasLogAnalysisReadCapabilities,
@ -55,11 +44,7 @@ export const LogEntryCategoriesPageContent = () => {
}
}, [fetchJobStatus, hasLogAnalysisReadCapabilities]);
if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (hasFailedLoadingSource) {
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
} else if (!hasLogAnalysisCapabilites) {
if (!hasLogAnalysisCapabilites) {
return <SubscriptionSplashContent />;
} else if (!hasLogAnalysisReadCapabilities) {
return <MissingResultsPrivilegesPrompt />;

View file

@ -7,30 +7,46 @@
import React from 'react';
import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout';
import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => {
const { sourceId, resolvedSourceConfiguration } = useLogSourceContext();
const {
hasFailedLoading,
isLoading,
isUninitialized,
latestLoadSourceFailures,
loadSource,
resolvedSourceConfiguration,
sourceId,
} = useLogSourceContext();
const { space } = useActiveKibanaSpace();
// This is a rather crude way of guarding the dependent providers against
// arguments that are only made available asynchronously. Ideally, we'd use
// React concurrent mode and Suspense in order to handle that more gracefully.
if (!resolvedSourceConfiguration || space == null) {
if (space == null) {
return null;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadSourceFailures} onRetry={loadSource} />;
} else if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (resolvedSourceConfiguration != null) {
return (
<LogEntryCategoriesModuleProvider
indexPattern={resolvedSourceConfiguration.indices}
sourceId={sourceId}
spaceId={space.id}
timestampField={resolvedSourceConfiguration.timestampField}
runtimeMappings={resolvedSourceConfiguration.runtimeMappings}
>
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider>
</LogEntryCategoriesModuleProvider>
);
} else {
return null;
}
return (
<LogEntryCategoriesModuleProvider
indexPattern={resolvedSourceConfiguration.indices}
sourceId={sourceId}
spaceId={space.id}
timestampField={resolvedSourceConfiguration.timestampField}
runtimeMappings={resolvedSourceConfiguration.runtimeMappings}
>
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider>
</LogEntryCategoriesModuleProvider>
);
};

View file

@ -6,9 +6,8 @@
*/
import { i18n } from '@kbn/i18n';
import React, { memo, useEffect, useCallback } from 'react';
import React, { memo, useCallback, useEffect } from 'react';
import useInterval from 'react-use/lib/useInterval';
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { isJobStatusWithResults } from '../../../../common/log_analysis';
import { LoadingPage } from '../../../components/loading_page';
import {
@ -20,26 +19,16 @@ import {
LogAnalysisSetupFlyout,
useLogAnalysisSetupFlyoutStateContext,
} from '../../../components/logging/log_analysis_setup/setup_flyout';
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { SubscriptionSplashContent } from '../../../components/subscription_splash_content';
import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis';
import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { LogEntryRateResultsContent } from './page_results_content';
import { LogEntryRateSetupContent } from './page_setup_content';
const JOB_STATUS_POLLING_INTERVAL = 30000;
export const LogEntryRatePageContent = memo(() => {
const {
hasFailedLoadingSource,
isLoading,
isUninitialized,
loadSource,
loadSourceFailureMessage,
} = useLogSourceContext();
const {
hasLogAnalysisCapabilites,
hasLogAnalysisReadCapabilities,
@ -93,11 +82,7 @@ export const LogEntryRatePageContent = memo(() => {
}
}, JOB_STATUS_POLLING_INTERVAL);
if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (hasFailedLoadingSource) {
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
} else if (!hasLogAnalysisCapabilites) {
if (!hasLogAnalysisCapabilites) {
return <SubscriptionSplashContent />;
} else if (!hasLogAnalysisReadCapabilities) {
return <MissingResultsPrivilegesPrompt />;

View file

@ -7,42 +7,58 @@
import React from 'react';
import { LogAnalysisSetupFlyoutStateProvider } from '../../../components/logging/log_analysis_setup/setup_flyout';
import { LogSourceErrorPage } from '../../../components/logging/log_source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { LogEntryCategoriesModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_categories';
import { LogEntryRateModuleProvider } from '../../../containers/logs/log_analysis/modules/log_entry_rate';
import { LogFlyout } from '../../../containers/logs/log_flyout';
import { useLogSourceContext } from '../../../containers/logs/log_source';
import { useActiveKibanaSpace } from '../../../hooks/use_kibana_space';
import { LogFlyout } from '../../../containers/logs/log_flyout';
export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => {
const { sourceId, resolvedSourceConfiguration } = useLogSourceContext();
const {
hasFailedLoading,
isLoading,
isUninitialized,
latestLoadSourceFailures,
loadSource,
resolvedSourceConfiguration,
sourceId,
} = useLogSourceContext();
const { space } = useActiveKibanaSpace();
// This is a rather crude way of guarding the dependent providers against
// arguments that are only made available asynchronously. Ideally, we'd use
// React concurrent mode and Suspense in order to handle that more gracefully.
if (!resolvedSourceConfiguration || space == null) {
if (space == null) {
return null;
}
return (
<LogFlyout.Provider>
<LogEntryRateModuleProvider
indexPattern={resolvedSourceConfiguration.indices ?? ''}
sourceId={sourceId}
spaceId={space.id}
timestampField={resolvedSourceConfiguration.timestampField ?? ''}
runtimeMappings={resolvedSourceConfiguration.runtimeMappings}
>
<LogEntryCategoriesModuleProvider
indexPattern={resolvedSourceConfiguration.indices ?? ''}
} else if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadSourceFailures} onRetry={loadSource} />;
} else if (resolvedSourceConfiguration != null) {
return (
<LogFlyout.Provider>
<LogEntryRateModuleProvider
indexPattern={resolvedSourceConfiguration.indices}
sourceId={sourceId}
spaceId={space.id}
timestampField={resolvedSourceConfiguration.timestampField ?? ''}
timestampField={resolvedSourceConfiguration.timestampField}
runtimeMappings={resolvedSourceConfiguration.runtimeMappings}
>
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider>
</LogEntryCategoriesModuleProvider>
</LogEntryRateModuleProvider>
</LogFlyout.Provider>
);
<LogEntryCategoriesModuleProvider
indexPattern={resolvedSourceConfiguration.indices}
sourceId={sourceId}
spaceId={space.id}
timestampField={resolvedSourceConfiguration.timestampField}
runtimeMappings={resolvedSourceConfiguration.runtimeMappings}
>
<LogAnalysisSetupFlyoutStateProvider>{children}</LogAnalysisSetupFlyoutStateProvider>
</LogEntryCategoriesModuleProvider>
</LogEntryRateModuleProvider>
</LogFlyout.Provider>
);
} else {
return null;
}
};

View file

@ -28,15 +28,30 @@ export const IndexPatternSelector: React.FC<{
fetchIndexPatternTitles();
}, [fetchIndexPatternTitles]);
const availableOptions = useMemo<IndexPatternOption[]>(
() =>
availableIndexPatterns.map(({ id, title }) => ({
const availableOptions = useMemo<IndexPatternOption[]>(() => {
const options = [
...availableIndexPatterns.map(({ id, title }) => ({
key: id,
label: title,
value: id,
})),
[availableIndexPatterns]
);
...(indexPatternId == null || availableIndexPatterns.some(({ id }) => id === indexPatternId)
? []
: [
{
key: indexPatternId,
label: i18n.translate('xpack.infra.logSourceConfiguration.missingIndexPatternLabel', {
defaultMessage: `Missing index pattern {indexPatternId}`,
values: {
indexPatternId,
},
}),
value: indexPatternId,
},
]),
];
return options;
}, [availableIndexPatterns, indexPatternId]);
const selectedOptions = useMemo<IndexPatternOption[]>(
() => availableOptions.filter(({ key }) => key === indexPatternId),

View file

@ -6,6 +6,7 @@
*/
import { useMemo } from 'react';
import { SavedObjectNotFound } from '../../../../../../../src/plugins/kibana_utils/common';
import { useUiTracker } from '../../../../../observability/public';
import {
LogIndexNameReference,
@ -45,9 +46,20 @@ export const useLogIndicesFormElement = (initialValue: LogIndicesFormState) => {
return emptyStringErrors;
}
const indexPatternErrors = validateIndexPattern(
await indexPatternService.get(logIndices.indexPatternId)
);
const indexPatternErrors = await indexPatternService
.get(logIndices.indexPatternId)
.then(validateIndexPattern, (error): FormValidationError[] => {
if (error instanceof SavedObjectNotFound) {
return [
{
type: 'missing_index_pattern' as const,
indexPatternId: logIndices.indexPatternId,
},
];
} else {
throw error;
}
});
if (indexPatternErrors.length > 0) {
trackIndexPatternValidationError({

View file

@ -88,6 +88,16 @@ export const LogSourceConfigurationFormError: React.FC<{ error: FormValidationEr
defaultMessage="The index pattern must not be a rollup index pattern."
/>
);
} else if (error.type === 'missing_index_pattern') {
return (
<FormattedMessage
id="xpack.infra.logSourceConfiguration.missingIndexPatternErrorMessage"
defaultMessage="The index pattern {indexPatternId} must exist."
values={{
indexPatternId: <EuiCode>{error.indexPatternId}</EuiCode>,
}}
/>
);
} else {
return null;
}

View file

@ -43,9 +43,10 @@ export const LogsSettingsPage = () => {
const {
sourceConfiguration: source,
hasFailedLoadingSource,
isLoading,
isUninitialized,
updateSourceConfiguration,
updateSource,
resolvedSourceConfiguration,
} = useLogSourceContext();
@ -65,9 +66,9 @@ export const LogsSettingsPage = () => {
} = useLogSourceConfigurationFormState(source?.configuration);
const persistUpdates = useCallback(async () => {
await updateSourceConfiguration(formState);
await updateSource(formState);
sourceConfigurationFormElement.resetValue();
}, [updateSourceConfiguration, sourceConfigurationFormElement, formState]);
}, [updateSource, sourceConfigurationFormElement, formState]);
const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [
shouldAllowEdit,
@ -77,7 +78,7 @@ export const LogsSettingsPage = () => {
if ((isLoading || isUninitialized) && !resolvedSourceConfiguration) {
return <SourceLoadingPage />;
}
if (!source?.configuration) {
if (hasFailedLoadingSource) {
return null;
}

View file

@ -45,6 +45,11 @@ export interface RollupIndexPatternValidationError {
indexPatternTitle: string;
}
export interface MissingIndexPatternValidationError {
type: 'missing_index_pattern';
indexPatternId: string;
}
export type FormValidationError =
| GenericValidationError
| ChildFormValidationError
@ -53,7 +58,8 @@ export type FormValidationError =
| MissingTimestampFieldValidationError
| MissingMessageFieldValidationError
| InvalidMessageFieldTypeValidationError
| RollupIndexPatternValidationError;
| RollupIndexPatternValidationError
| MissingIndexPatternValidationError;
export const validateStringNotEmpty = (fieldName: string, value: string): FormValidationError[] =>
value === '' ? [{ type: 'empty_field', fieldName }] : [];

View file

@ -6,26 +6,26 @@
*/
import React from 'react';
import { SourceErrorPage } from '../../../components/source_error_page';
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 { LogsPageNoIndicesContent } from './page_no_indices_content';
import { useLogSourceContext } from '../../../containers/logs/log_source';
export const StreamPageContent: React.FunctionComponent = () => {
const {
hasFailedLoadingSource,
hasFailedLoading,
isLoading,
isUninitialized,
loadSource,
loadSourceFailureMessage,
latestLoadSourceFailures,
sourceStatus,
} = useLogSourceContext();
if (isLoading || isUninitialized) {
return <SourceLoadingPage />;
} else if (hasFailedLoadingSource) {
return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />;
} else if (hasFailedLoading) {
return <LogSourceErrorPage errors={latestLoadSourceFailures} onRetry={loadSource} />;
} else if (sourceStatus?.logIndexStatus !== 'missing') {
return <LogsPageLogsContent />;
} else {

View file

@ -256,14 +256,18 @@ export interface RejectedPromiseState<ResolvedValue, RejectedValue> {
value: RejectedValue;
}
type SettledPromise<ResolvedValue, RejectedValue> =
export type SettledPromiseState<ResolvedValue, RejectedValue> =
| ResolvedPromiseState<ResolvedValue>
| RejectedPromiseState<ResolvedValue, RejectedValue>;
type PromiseState<ResolvedValue, RejectedValue = unknown> =
export type PromiseState<ResolvedValue, RejectedValue = unknown> =
| UninitializedPromiseState
| PendingPromiseState<ResolvedValue>
| SettledPromise<ResolvedValue, RejectedValue>;
| SettledPromiseState<ResolvedValue, RejectedValue>;
export const isRejectedPromiseState = (
promiseState: PromiseState<any, any>
): promiseState is RejectedPromiseState<any, any> => promiseState.state === 'rejected';
interface CancelablePromise<ResolvedValue> {
// reject the promise prematurely with a CanceledPromiseError

View file

@ -46,7 +46,7 @@ export const evaluateCondition = async ({
condition: InventoryMetricConditions;
nodeType: InventoryItemType;
source: InfraSource;
logQueryFields: LogQueryFields;
logQueryFields: LogQueryFields | undefined;
esClient: ElasticsearchClient;
compositeSize: number;
filterQuery?: string;
@ -115,7 +115,7 @@ const getData = async (
metric: SnapshotMetricType,
timerange: InfraTimerangeInput,
source: InfraSource,
logQueryFields: LogQueryFields,
logQueryFields: LogQueryFields | undefined,
compositeSize: number,
filterQuery?: string,
customMetric?: SnapshotCustomMetricInput
@ -144,8 +144,8 @@ const getData = async (
client,
snapshotRequest,
source,
logQueryFields,
compositeSize
compositeSize,
logQueryFields
);
if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state

View file

@ -68,11 +68,13 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
sourceId || 'default'
);
const logQueryFields = await libs.getLogQueryFields(
sourceId || 'default',
services.savedObjectsClient,
services.scopedClusterClient.asCurrentUser
);
const logQueryFields = await libs
.getLogQueryFields(
sourceId || 'default',
services.savedObjectsClient,
services.scopedClusterClient.asCurrentUser
)
.catch(() => undefined);
const compositeSize = libs.configuration.inventory.compositeSize;

View file

@ -4,7 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/* eslint-disable max-classes-per-file */
export class NotFoundError extends Error {
constructor(message?: string) {
super(message);
@ -18,3 +20,11 @@ export class AnomalyThresholdRangeError extends Error {
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class SavedObjectReferenceResolutionError extends Error {
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'SavedObjectReferenceResolutionError';
}
}

View file

@ -0,0 +1,100 @@
/*
* 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 { InfraSourceConfiguration } from '../../../common/source_configuration/source_configuration';
import {
extractSavedObjectReferences,
resolveSavedObjectReferences,
} from './saved_object_references';
describe('extractSavedObjectReferences function', () => {
it('extracts log index pattern references', () => {
const { attributes, references } = extractSavedObjectReferences(
sourceConfigurationWithIndexPatternReference
);
expect(references).toMatchObject([{ id: 'INDEX_PATTERN_ID' }]);
expect(attributes).toHaveProperty(['logIndices', 'indexPatternId'], references[0].name);
});
it('ignores log index name references', () => {
const { attributes, references } = extractSavedObjectReferences(
sourceConfigurationWithIndexNameReference
);
expect(references).toHaveLength(0);
expect(attributes).toHaveProperty(['logIndices', 'indexName'], 'INDEX_NAME');
});
});
describe('resolveSavedObjectReferences function', () => {
it('is the inverse operation of extractSavedObjectReferences', () => {
const { attributes, references } = extractSavedObjectReferences(
sourceConfigurationWithIndexPatternReference
);
const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, references);
expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexPatternReference);
});
it('ignores additional saved object references', () => {
const { attributes, references } = extractSavedObjectReferences(
sourceConfigurationWithIndexPatternReference
);
const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, [
...references,
{ name: 'log_index_pattern_1', id: 'SOME_ID', type: 'index-pattern' },
]);
expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexPatternReference);
});
it('ignores log index name references', () => {
const { attributes, references } = extractSavedObjectReferences(
sourceConfigurationWithIndexNameReference
);
const resolvedSourceConfiguration = resolveSavedObjectReferences(attributes, [
...references,
{ name: 'log_index_pattern_0', id: 'SOME_ID', type: 'index-pattern' },
]);
expect(resolvedSourceConfiguration).toEqual(sourceConfigurationWithIndexNameReference);
});
});
const sourceConfigurationWithIndexPatternReference: InfraSourceConfiguration = {
name: 'NAME',
description: 'DESCRIPTION',
fields: {
container: 'CONTAINER_FIELD',
host: 'HOST_FIELD',
message: ['MESSAGE_FIELD'],
pod: 'POD_FIELD',
tiebreaker: 'TIEBREAKER_FIELD',
timestamp: 'TIMESTAMP_FIELD',
},
logColumns: [],
logIndices: {
type: 'index_pattern',
indexPatternId: 'INDEX_PATTERN_ID',
},
metricAlias: 'METRIC_ALIAS',
anomalyThreshold: 0,
inventoryDefaultView: 'INVENTORY_DEFAULT_VIEW',
metricsExplorerDefaultView: 'METRICS_EXPLORER_DEFAULT_VIEW',
};
const sourceConfigurationWithIndexNameReference: InfraSourceConfiguration = {
...sourceConfigurationWithIndexPatternReference,
logIndices: {
type: 'index_name',
indexName: 'INDEX_NAME',
},
};

View file

@ -0,0 +1,113 @@
/*
* 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 {
InfraSavedSourceConfiguration,
InfraSourceConfiguration,
} from '../../../common/source_configuration/source_configuration';
import { SavedObjectReferenceResolutionError } from './errors';
const logIndexPatternReferenceName = 'log_index_pattern_0';
interface SavedObjectAttributesWithReferences<SavedObjectAttributes> {
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<InfraSourceConfiguration> =>
[extractLogIndicesSavedObjectReferences].reduce<
SavedObjectAttributesWithReferences<InfraSourceConfiguration>
>(
({ 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].reduce<InfraSavedSourceConfiguration>(
(accumulatedAttributes, resolve) => resolve(accumulatedAttributes, references),
attributes
);
const extractLogIndicesSavedObjectReferences = (
sourceConfiguration: InfraSourceConfiguration
): SavedObjectAttributesWithReferences<InfraSourceConfiguration> => {
if (sourceConfiguration.logIndices.type === 'index_pattern') {
const logIndexPatternReference: SavedObjectReference = {
id: sourceConfiguration.logIndices.indexPatternId,
type: 'index-pattern',
name: logIndexPatternReferenceName,
};
const attributes: InfraSourceConfiguration = {
...sourceConfiguration,
logIndices: {
...sourceConfiguration.logIndices,
indexPatternId: logIndexPatternReference.name,
},
};
return {
attributes,
references: [logIndexPatternReference],
};
} else {
return {
attributes: sourceConfiguration,
references: [],
};
}
};
const resolveLogIndicesSavedObjectReferences = (
attributes: InfraSavedSourceConfiguration,
references: SavedObjectReference[]
): InfraSavedSourceConfiguration => {
if (attributes.logIndices?.type === 'index_pattern') {
const logIndexPatternReference = references.find(
(reference) => reference.name === logIndexPatternReferenceName
);
if (logIndexPatternReference == null) {
throw new SavedObjectReferenceResolutionError(
`Failed to resolve log index pattern reference "${logIndexPatternReferenceName}".`
);
}
return {
...attributes,
logIndices: {
...attributes.logIndices,
indexPatternId: logIndexPatternReference.id,
},
};
} else {
return attributes;
}
};

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { SavedObject } from '../../../../../../src/core/server';
import { infraSourceConfigurationSavedObjectName } from './saved_object_type';
import { InfraSources } from './sources';
describe('the InfraSources lib', () => {
@ -18,9 +20,10 @@ describe('the InfraSources lib', () => {
id: 'TEST_ID',
version: 'foo',
updated_at: '2000-01-01T00:00:00.000Z',
type: infraSourceConfigurationSavedObjectName,
attributes: {
metricAlias: 'METRIC_ALIAS',
logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' },
logIndices: { type: 'index_pattern', indexPatternId: 'log_index_pattern_0' },
fields: {
container: 'CONTAINER',
host: 'HOST',
@ -29,6 +32,13 @@ describe('the InfraSources lib', () => {
timestamp: 'TIMESTAMP',
},
},
references: [
{
id: 'LOG_INDEX_PATTERN',
name: 'log_index_pattern_0',
type: 'index-pattern',
},
],
});
expect(
@ -39,7 +49,7 @@ describe('the InfraSources lib', () => {
updatedAt: 946684800000,
configuration: {
metricAlias: 'METRIC_ALIAS',
logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' },
logIndices: { type: 'index_pattern', indexPatternId: 'LOG_INDEX_PATTERN' },
fields: {
container: 'CONTAINER',
host: 'HOST',
@ -70,12 +80,14 @@ describe('the InfraSources lib', () => {
const request: any = createRequestContext({
id: 'TEST_ID',
version: 'foo',
type: infraSourceConfigurationSavedObjectName,
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {
fields: {
container: 'CONTAINER',
},
},
references: [],
});
expect(
@ -106,8 +118,10 @@ describe('the InfraSources lib', () => {
const request: any = createRequestContext({
id: 'TEST_ID',
version: 'foo',
type: infraSourceConfigurationSavedObjectName,
updated_at: '2000-01-01T00:00:00.000Z',
attributes: {},
references: [],
});
expect(
@ -140,7 +154,7 @@ const createMockStaticConfiguration = (sources: any) => ({
sources,
});
const createRequestContext = (savedObject?: any) => {
const createRequestContext = (savedObject?: SavedObject<unknown>) => {
return {
core: {
savedObjects: {

View file

@ -5,26 +5,29 @@
* 2.0.
*/
import { failure } from 'io-ts/lib/PathReporter';
import { identity, constant } from 'fp-ts/lib/function';
import { fold, map } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { failure } from 'io-ts/lib/PathReporter';
import { inRange } from 'lodash';
import { SavedObjectsClientContract } from 'src/core/server';
import { defaultSourceConfiguration } from './defaults';
import { AnomalyThresholdRangeError, NotFoundError } from './errors';
import { infraSourceConfigurationSavedObjectName } from './saved_object_type';
import { SavedObject, SavedObjectsClientContract } from 'src/core/server';
import {
InfraSavedSourceConfiguration,
InfraSource,
InfraSourceConfiguration,
InfraStaticSourceConfiguration,
pickSavedSourceConfiguration,
SourceConfigurationSavedObjectRuntimeType,
InfraSource,
sourceConfigurationConfigFilePropertiesRT,
SourceConfigurationConfigFileProperties,
sourceConfigurationConfigFilePropertiesRT,
SourceConfigurationSavedObjectRuntimeType,
} from '../../../common/source_configuration/source_configuration';
import { InfraConfig } from '../../../server';
import { defaultSourceConfiguration } from './defaults';
import { AnomalyThresholdRangeError, NotFoundError } from './errors';
import {
extractSavedObjectReferences,
resolveSavedObjectReferences,
} from './saved_object_references';
import { infraSourceConfigurationSavedObjectName } from './saved_object_type';
interface Libs {
config: InfraConfig;
@ -113,13 +116,13 @@ export class InfraSources {
staticDefaultSourceConfiguration,
source
);
const { attributes, references } = extractSavedObjectReferences(newSourceConfiguration);
const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
await savedObjectsClient.create(
infraSourceConfigurationSavedObjectName,
pickSavedSourceConfiguration(newSourceConfiguration) as any,
{ id: sourceId }
)
await savedObjectsClient.create(infraSourceConfigurationSavedObjectName, attributes, {
id: sourceId,
references,
})
);
return {
@ -158,19 +161,19 @@ export class InfraSources {
configuration,
sourceProperties
);
const { attributes, references } = extractSavedObjectReferences(
updatedSourceConfigurationAttributes
);
const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration(
// update() will perform a deep merge. We use create() with overwrite: true instead. mergeSourceConfiguration()
// ensures the correct and intended merging of properties.
await savedObjectsClient.create(
infraSourceConfigurationSavedObjectName,
pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any,
{
id: sourceId,
version,
overwrite: true,
}
)
await savedObjectsClient.create(infraSourceConfigurationSavedObjectName, attributes, {
id: sourceId,
overwrite: true,
references,
version,
})
);
return {
@ -267,7 +270,7 @@ const mergeSourceConfiguration = (
first
);
export const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknown) =>
export const convertSavedObjectToSavedSourceConfiguration = (savedObject: SavedObject<unknown>) =>
pipe(
SourceConfigurationSavedObjectRuntimeType.decode(savedObject),
map((savedSourceConfiguration) => ({
@ -275,7 +278,10 @@ export const convertSavedObjectToSavedSourceConfiguration = (savedObject: unknow
version: savedSourceConfiguration.version,
updatedAt: savedSourceConfiguration.updated_at,
origin: 'stored' as 'stored',
configuration: savedSourceConfiguration.attributes,
configuration: resolveSavedObjectReferences(
savedSourceConfiguration.attributes,
savedObject.references
),
})),
fold((errors) => {
throw new Error(failure(errors).join('\n'));

View file

@ -41,11 +41,13 @@ 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
);
const logQueryFields = await libs
.getLogQueryFields(
snapshotRequest.sourceId,
requestContext.core.savedObjects.client,
requestContext.core.elasticsearch.client.asCurrentUser
)
.catch(() => undefined);
UsageCollector.countNode(snapshotRequest.nodeType);
const client = createSearchClient(requestContext, framework);
@ -55,8 +57,8 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
client,
snapshotRequest,
source,
logQueryFields,
compositeSize
compositeSize,
logQueryFields
);
return response.ok({
body: SnapshotNodeResponseRT.encode(snapshotResponse),

View file

@ -53,21 +53,25 @@ export const getNodes = async (
client: ESSearchClient,
snapshotRequest: SnapshotRequest,
source: InfraSource,
logQueryFields: LogQueryFields,
compositeSize: number
compositeSize: number,
logQueryFields?: LogQueryFields
) => {
let nodes;
if (snapshotRequest.metrics.find((metric) => metric.type === 'logRate')) {
// *Only* the log rate metric has been requested
if (snapshotRequest.metrics.length === 1) {
nodes = await transformAndQueryData({
client,
snapshotRequest,
source,
compositeSize,
sourceOverrides: logQueryFields,
});
if (logQueryFields != null) {
nodes = await transformAndQueryData({
client,
snapshotRequest,
source,
compositeSize,
sourceOverrides: logQueryFields,
});
} else {
nodes = { nodes: [], interval: '60s' };
}
} else {
// A scenario whereby a single host might be shipping metrics and logs.
const metricsWithoutLogsMetrics = snapshotRequest.metrics.filter(
@ -79,13 +83,16 @@ export const getNodes = async (
source,
compositeSize,
});
const logRateNodes = await transformAndQueryData({
client,
snapshotRequest: { ...snapshotRequest, metrics: [{ type: 'logRate' }] },
source,
compositeSize,
sourceOverrides: logQueryFields,
});
const logRateNodes =
logQueryFields != null
? await transformAndQueryData({
client,
snapshotRequest: { ...snapshotRequest, metrics: [{ type: 'logRate' }] },
source,
compositeSize,
sourceOverrides: logQueryFields,
})
: { nodes: [], interval: '60s' };
// Merge nodes where possible - e.g. a single host is shipping metrics and logs
const mergedNodes = nodesWithoutLogsMetrics.nodes.map((node) => {
const logRateNode = logRateNodes.nodes.find(