mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -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 SYNTHETIC_CHECK_STEPS_ROUTE = '/journey/:checkGroupId/steps';
|
||||||
|
|
||||||
|
export const MAPPING_ERROR_ROUTE = '/mapping-error';
|
||||||
|
|
||||||
export enum STATUS {
|
export enum STATUS {
|
||||||
UP = 'up',
|
UP = 'up',
|
||||||
DOWN = 'down',
|
DOWN = 'down',
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { MonitorListComponent } from './monitor_list';
|
||||||
import { useUrlParams } from '../../../hooks';
|
import { useUrlParams } from '../../../hooks';
|
||||||
import { UptimeRefreshContext } from '../../../contexts';
|
import { UptimeRefreshContext } from '../../../contexts';
|
||||||
import { getConnectorsAction, getMonitorAlertsAction } from '../../../state/alerts/alerts';
|
import { getConnectorsAction, getMonitorAlertsAction } from '../../../state/alerts/alerts';
|
||||||
|
import { useMappingCheck } from '../../../hooks/use_mapping_check';
|
||||||
|
|
||||||
export interface MonitorListProps {
|
export interface MonitorListProps {
|
||||||
filters?: string;
|
filters?: string;
|
||||||
|
@ -41,6 +42,7 @@ export const MonitorList: React.FC<MonitorListProps> = (props) => {
|
||||||
const { lastRefresh } = useContext(UptimeRefreshContext);
|
const { lastRefresh } = useContext(UptimeRefreshContext);
|
||||||
|
|
||||||
const monitorList = useSelector(monitorListSelector);
|
const monitorList = useSelector(monitorListSelector);
|
||||||
|
useMappingCheck(monitorList.error);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
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 {
|
export enum UptimePage {
|
||||||
Overview = 'Overview',
|
Overview = 'Overview',
|
||||||
|
MappingError = 'MappingError',
|
||||||
Monitor = 'Monitor',
|
Monitor = 'Monitor',
|
||||||
Settings = 'Settings',
|
Settings = 'Settings',
|
||||||
Certificates = 'Certificates',
|
Certificates = 'Certificates',
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { MappingErrorPage } from './mapping_error';
|
||||||
export { MonitorPage } from './monitor';
|
export { MonitorPage } from './monitor';
|
||||||
export { StepDetailPage } from './synthetics/step_detail_page';
|
export { StepDetailPage } from './synthetics/step_detail_page';
|
||||||
export { SettingsPage } from './settings';
|
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 { i18n } from '@kbn/i18n';
|
||||||
import {
|
import {
|
||||||
CERTIFICATES_ROUTE,
|
CERTIFICATES_ROUTE,
|
||||||
|
MAPPING_ERROR_ROUTE,
|
||||||
MONITOR_ROUTE,
|
MONITOR_ROUTE,
|
||||||
OVERVIEW_ROUTE,
|
OVERVIEW_ROUTE,
|
||||||
SETTINGS_ROUTE,
|
SETTINGS_ROUTE,
|
||||||
STEP_DETAIL_ROUTE,
|
STEP_DETAIL_ROUTE,
|
||||||
SYNTHETIC_CHECK_STEPS_ROUTE,
|
SYNTHETIC_CHECK_STEPS_ROUTE,
|
||||||
} from '../common/constants';
|
} from '../common/constants';
|
||||||
import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages';
|
import { MappingErrorPage, MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages';
|
||||||
import { CertificatesPage } from './pages/certificates';
|
import { CertificatesPage } from './pages/certificates';
|
||||||
import { UptimePage, useUptimeTelemetry } from './hooks';
|
import { UptimePage, useUptimeTelemetry } from './hooks';
|
||||||
import { OverviewPageComponent } from './pages/overview';
|
import { OverviewPageComponent } from './pages/overview';
|
||||||
|
@ -142,6 +143,26 @@ const Routes: RouteProps[] = [
|
||||||
rightSideItems: [<UptimeDatePicker />],
|
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'>> = ({
|
const RouteInit: React.FC<Pick<RouteProps, 'path' | 'title' | 'telemetryId'>> = ({
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
|
||||||
options: {
|
options: {
|
||||||
tags: ['access:uptime-read'],
|
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 } =
|
const { dateRangeStart, dateRangeEnd, filters, pagination, statusFilter, pageSize, query } =
|
||||||
request.query;
|
request.query;
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
|
||||||
? JSON.parse(decodeURIComponent(pagination))
|
? JSON.parse(decodeURIComponent(pagination))
|
||||||
: CONTEXT_DEFAULTS.CURSOR_PAGINATION;
|
: CONTEXT_DEFAULTS.CURSOR_PAGINATION;
|
||||||
|
|
||||||
|
try {
|
||||||
const result = await libs.requests.getMonitorStates({
|
const result = await libs.requests.getMonitorStates({
|
||||||
uptimeEsClient,
|
uptimeEsClient,
|
||||||
dateRangeStart,
|
dateRangeStart,
|
||||||
|
@ -43,12 +44,20 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({
|
||||||
pageSize,
|
pageSize,
|
||||||
filters,
|
filters,
|
||||||
query,
|
query,
|
||||||
// this is added to make typescript happy,
|
statusFilter,
|
||||||
// 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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks';
|
||||||
import { getSha256 } from '../../../api_integration/apis/uptime/rest/helper/make_tls';
|
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) => {
|
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
const { uptime } = getPageObjects(['uptime']);
|
const { uptime } = getPageObjects(['uptime']);
|
||||||
const uptimeService = getService('uptime');
|
const uptimeService = getService('uptime');
|
||||||
|
|
||||||
|
const esArchiver = getService('esArchiver');
|
||||||
const es = getService('es');
|
const es = getService('es');
|
||||||
|
|
||||||
describe('certificates', function () {
|
describe('certificates', function () {
|
||||||
describe('empty certificates', function () {
|
describe('empty certificates', function () {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
await esArchiver.load(BLANK_INDEX_PATH);
|
||||||
await makeCheck({ es });
|
await makeCheck({ es });
|
||||||
await uptime.goToRoot(true);
|
await uptime.goToRoot(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await esArchiver.unload(BLANK_INDEX_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
it('go to certs page', async () => {
|
it('go to certs page', async () => {
|
||||||
await uptimeService.common.waitUntilDataIsLoaded();
|
await uptimeService.common.waitUntilDataIsLoaded();
|
||||||
await uptimeService.cert.hasViewCertButton();
|
await uptimeService.cert.hasViewCertButton();
|
||||||
|
@ -34,10 +42,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||||
|
|
||||||
describe('with certs', function () {
|
describe('with certs', function () {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
await esArchiver.load(BLANK_INDEX_PATH);
|
||||||
await makeCheck({ es, tls: true });
|
await makeCheck({ es, tls: true });
|
||||||
await uptime.goToRoot(true);
|
await uptime.goToRoot(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await esArchiver.unload(BLANK_INDEX_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await makeCheck({ es, tls: true });
|
await makeCheck({ es, tls: true });
|
||||||
});
|
});
|
||||||
|
|
|
@ -80,5 +80,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
|
||||||
loadTestFile(require.resolve('./ml_anomaly'));
|
loadTestFile(require.resolve('./ml_anomaly'));
|
||||||
loadTestFile(require.resolve('./feature_controls'));
|
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');
|
await testSubjects.missingOrFail('data-missing');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async hasMappingsError() {
|
||||||
|
return testSubjects.exists('xpack.uptime.mappingsErrorPage');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue