mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Uptime] Redirect to error page when Heartbeat mappings are missing (#110857)
* Initial PoC of redirect on mapping error is working. * Update copy. Add comments. * Include headline element for page title. * Create mappings for failing functional tests. * Add functional test for mappings error page. * Add mapping for certs check.
This commit is contained in:
parent
6235371cda
commit
26d19e7fd1
13 changed files with 272 additions and 16 deletions
|
@ -17,6 +17,8 @@ export const STEP_DETAIL_ROUTE = '/journey/:checkGroupId/step/:stepIndex';
|
|||
|
||||
export const SYNTHETIC_CHECK_STEPS_ROUTE = '/journey/:checkGroupId/steps';
|
||||
|
||||
export const MAPPING_ERROR_ROUTE = '/mapping-error';
|
||||
|
||||
export enum STATUS {
|
||||
UP = 'up',
|
||||
DOWN = 'down',
|
||||
|
|
|
@ -13,6 +13,7 @@ import { MonitorListComponent } from './monitor_list';
|
|||
import { useUrlParams } from '../../../hooks';
|
||||
import { UptimeRefreshContext } from '../../../contexts';
|
||||
import { getConnectorsAction, getMonitorAlertsAction } from '../../../state/alerts/alerts';
|
||||
import { useMappingCheck } from '../../../hooks/use_mapping_check';
|
||||
|
||||
export interface MonitorListProps {
|
||||
filters?: string;
|
||||
|
@ -41,6 +42,7 @@ export const MonitorList: React.FC<MonitorListProps> = (props) => {
|
|||
const { lastRefresh } = useContext(UptimeRefreshContext);
|
||||
|
||||
const monitorList = useSelector(monitorListSelector);
|
||||
useMappingCheck(monitorList.error);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
|
|
53
x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts
Normal file
53
x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { shouldRedirect } from './use_mapping_check';
|
||||
|
||||
describe('useMappingCheck', () => {
|
||||
describe('should redirect', () => {
|
||||
it('returns true for appropriate error', () => {
|
||||
const error = {
|
||||
request: {},
|
||||
response: {},
|
||||
body: {
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message:
|
||||
'[search_phase_execution_exception: [illegal_argument_exception] Reason: Text fields are not optimised for operations that require per-document field data like aggregations and sorting, so these operations are disabled by default. Please use a keyword field instead. Alternatively, set fielddata=true on [monitor.id] in order to load field data by uninverting the inverted index. Note that this can use significant memory.]: all shards failed',
|
||||
},
|
||||
name: 'Error',
|
||||
req: {},
|
||||
res: {},
|
||||
};
|
||||
expect(shouldRedirect(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(shouldRedirect(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for missing body', () => {
|
||||
expect(shouldRedirect({})).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for incorrect error string', () => {
|
||||
expect(shouldRedirect({ body: { error: 'not the right type' } })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for missing body message', () => {
|
||||
expect(shouldRedirect({ body: { error: 'Bad Request' } })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for incorrect error message', () => {
|
||||
expect(
|
||||
shouldRedirect({
|
||||
body: { error: 'Bad Request', message: 'Not the correct kind of error message' },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
43
x-pack/plugins/uptime/public/hooks/use_mapping_check.ts
Normal file
43
x-pack/plugins/uptime/public/hooks/use_mapping_check.ts
Normal file
|
@ -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 { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { MAPPING_ERROR_ROUTE } from '../../common/constants';
|
||||
|
||||
interface EsBadRequestError {
|
||||
body?: {
|
||||
error?: string;
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function contains(message: string, phrase: string) {
|
||||
return message.indexOf(phrase) !== -1;
|
||||
}
|
||||
|
||||
export function shouldRedirect(error?: EsBadRequestError) {
|
||||
if (!error || !error.body || error.body.error !== 'Bad Request' || !error.body.message) {
|
||||
return false;
|
||||
}
|
||||
const { message } = error.body;
|
||||
return (
|
||||
contains(message, 'search_phase_execution_exception') ||
|
||||
contains(message, 'Please use a keyword field instead.') ||
|
||||
contains(message, 'set fielddata=true')
|
||||
);
|
||||
}
|
||||
|
||||
export function useMappingCheck(error?: EsBadRequestError) {
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldRedirect(error)) {
|
||||
history.push(MAPPING_ERROR_ROUTE);
|
||||
}
|
||||
}, [error, history]);
|
||||
}
|
|
@ -12,6 +12,7 @@ import { API_URLS } from '../../common/constants';
|
|||
|
||||
export enum UptimePage {
|
||||
Overview = 'Overview',
|
||||
MappingError = 'MappingError',
|
||||
Monitor = 'Monitor',
|
||||
Settings = 'Settings',
|
||||
Certificates = 'Certificates',
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { MappingErrorPage } from './mapping_error';
|
||||
export { MonitorPage } from './monitor';
|
||||
export { StepDetailPage } from './synthetics/step_detail_page';
|
||||
export { SettingsPage } from './settings';
|
||||
|
|
78
x-pack/plugins/uptime/public/pages/mapping_error.tsx
Normal file
78
x-pack/plugins/uptime/public/pages/mapping_error.tsx
Normal file
|
@ -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 { EuiCode, EuiEmptyPrompt, EuiLink, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
|
||||
import { useTrackPageview } from '../../../observability/public';
|
||||
|
||||
export const MappingErrorPage = () => {
|
||||
useTrackPageview({ app: 'uptime', path: 'mapping-error' });
|
||||
useTrackPageview({ app: 'uptime', path: 'mapping-error', delay: 15000 });
|
||||
|
||||
const docLinks = useKibana().services.docLinks;
|
||||
|
||||
useBreadcrumbs([
|
||||
{
|
||||
text: i18n.translate('xpack.uptime.mappingErrorRoute.breadcrumb', {
|
||||
defaultMessage: 'Mapping error',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
data-test-subj="xpack.uptime.mappingsErrorPage"
|
||||
iconColor="danger"
|
||||
iconType="cross"
|
||||
title={
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.public.pages.mappingError.title"
|
||||
defaultMessage="Heartbeat mappings missing"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
body={
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.public.pages.mappingError.bodyMessage"
|
||||
defaultMessage="Incorrect mappings detected! Perhaps you forgot to run the heartbeat {setup} command?"
|
||||
values={{ setup: <EuiCode>setup</EuiCode> }}
|
||||
/>
|
||||
</p>
|
||||
{docLinks && (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.public.pages.mappingError.bodyDocsLink"
|
||||
defaultMessage="You can learn how to troubleshoot this issue in the {docsLink}."
|
||||
values={{
|
||||
docsLink: (
|
||||
<EuiLink
|
||||
href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/observability/${docLinks.DOC_LINK_VERSION}/troubleshoot-uptime-mapping-issues.html`}
|
||||
target="_blank"
|
||||
>
|
||||
docs
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -11,13 +11,14 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
CERTIFICATES_ROUTE,
|
||||
MAPPING_ERROR_ROUTE,
|
||||
MONITOR_ROUTE,
|
||||
OVERVIEW_ROUTE,
|
||||
SETTINGS_ROUTE,
|
||||
STEP_DETAIL_ROUTE,
|
||||
SYNTHETIC_CHECK_STEPS_ROUTE,
|
||||
} from '../common/constants';
|
||||
import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages';
|
||||
import { MappingErrorPage, MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages';
|
||||
import { CertificatesPage } from './pages/certificates';
|
||||
import { UptimePage, useUptimeTelemetry } from './hooks';
|
||||
import { OverviewPageComponent } from './pages/overview';
|
||||
|
@ -142,6 +143,26 @@ const Routes: RouteProps[] = [
|
|||
rightSideItems: [<UptimeDatePicker />],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.uptime.mappingErrorRoute.title', {
|
||||
defaultMessage: 'Synthetics | mapping error',
|
||||
}),
|
||||
path: MAPPING_ERROR_ROUTE,
|
||||
component: MappingErrorPage,
|
||||
dataTestSubj: 'uptimeMappingErrorPage',
|
||||
telemetryId: UptimePage.MappingError,
|
||||
pageHeader: {
|
||||
pageTitle: (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.uptime.mappingErrorRoute.pageHeader.title"
|
||||
defaultMessage="Mapping error"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
rightSideItems: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const RouteInit: React.FC<Pick<RouteProps, 'path' | 'title' | 'telemetryId'>> = ({
|
||||
|
|
|
@ -27,7 +27,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
|
|||
options: {
|
||||
tags: ['access:uptime-read'],
|
||||
},
|
||||
handler: async ({ uptimeEsClient, request }): Promise<any> => {
|
||||
handler: async ({ uptimeEsClient, request, response }): Promise<any> => {
|
||||
const { dateRangeStart, dateRangeEnd, filters, pagination, statusFilter, pageSize, query } =
|
||||
request.query;
|
||||
|
||||
|
@ -35,20 +35,29 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
|
|||
? JSON.parse(decodeURIComponent(pagination))
|
||||
: CONTEXT_DEFAULTS.CURSOR_PAGINATION;
|
||||
|
||||
const result = await libs.requests.getMonitorStates({
|
||||
uptimeEsClient,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
pagination: decodedPagination,
|
||||
pageSize,
|
||||
filters,
|
||||
query,
|
||||
// this is added to make typescript happy,
|
||||
// this sort of reassignment used to be further downstream but I've moved it here
|
||||
// because this code is going to be decomissioned soon
|
||||
statusFilter: statusFilter || undefined,
|
||||
});
|
||||
try {
|
||||
const result = await libs.requests.getMonitorStates({
|
||||
uptimeEsClient,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
pagination: decodedPagination,
|
||||
pageSize,
|
||||
filters,
|
||||
query,
|
||||
statusFilter,
|
||||
});
|
||||
|
||||
return result;
|
||||
return result;
|
||||
} catch (e) {
|
||||
/**
|
||||
* This particular error is usually indicative of a mapping problem within the user's
|
||||
* indices. It's relevant for the UI because we will be able to provide the user with a
|
||||
* tailored message to help them remediate this problem on their own with minimal effort.
|
||||
*/
|
||||
if (e.name === 'ResponseError') {
|
||||
return response.badRequest({ body: e });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -9,19 +9,27 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks';
|
||||
import { getSha256 } from '../../../api_integration/apis/uptime/rest/helper/make_tls';
|
||||
|
||||
const BLANK_INDEX_PATH = 'x-pack/test/functional/es_archives/uptime/blank';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const { uptime } = getPageObjects(['uptime']);
|
||||
const uptimeService = getService('uptime');
|
||||
|
||||
const esArchiver = getService('esArchiver');
|
||||
const es = getService('es');
|
||||
|
||||
describe('certificates', function () {
|
||||
describe('empty certificates', function () {
|
||||
before(async () => {
|
||||
await esArchiver.load(BLANK_INDEX_PATH);
|
||||
await makeCheck({ es });
|
||||
await uptime.goToRoot(true);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(BLANK_INDEX_PATH);
|
||||
});
|
||||
|
||||
it('go to certs page', async () => {
|
||||
await uptimeService.common.waitUntilDataIsLoaded();
|
||||
await uptimeService.cert.hasViewCertButton();
|
||||
|
@ -34,10 +42,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
|
||||
describe('with certs', function () {
|
||||
before(async () => {
|
||||
await esArchiver.load(BLANK_INDEX_PATH);
|
||||
await makeCheck({ es, tls: true });
|
||||
await uptime.goToRoot(true);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload(BLANK_INDEX_PATH);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await makeCheck({ es, tls: true });
|
||||
});
|
||||
|
|
|
@ -80,5 +80,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
|
|||
loadTestFile(require.resolve('./ml_anomaly'));
|
||||
loadTestFile(require.resolve('./feature_controls'));
|
||||
});
|
||||
|
||||
describe('mappings error state', () => {
|
||||
loadTestFile(require.resolve('./missing_mappings'));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
26
x-pack/test/functional/apps/uptime/missing_mappings.ts
Normal file
26
x-pack/test/functional/apps/uptime/missing_mappings.ts
Normal file
|
@ -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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks';
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const { common } = getPageObjects(['common']);
|
||||
const uptimeService = getService('uptime');
|
||||
|
||||
const es = getService('es');
|
||||
describe('missing mappings', function () {
|
||||
before(async () => {
|
||||
await makeCheck({ es });
|
||||
await common.navigateToApp('uptime');
|
||||
});
|
||||
|
||||
it('redirects to mappings error page', async () => {
|
||||
await uptimeService.common.hasMappingsError();
|
||||
});
|
||||
});
|
||||
};
|
|
@ -115,5 +115,8 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider
|
|||
await testSubjects.missingOrFail('data-missing');
|
||||
});
|
||||
},
|
||||
async hasMappingsError() {
|
||||
return testSubjects.exists('xpack.uptime.mappingsErrorPage');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue