Skip to content

Commit

Permalink
[Uptime] Redirect to error page when Heartbeat mappings are missing (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
justinkambic authored Sep 23, 2021
1 parent 6235371 commit 26d19e7
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 16 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/uptime/common/constants/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +42,7 @@ export const MonitorList: React.FC<MonitorListProps> = (props) => {
const { lastRefresh } = useContext(UptimeRefreshContext);

const monitorList = useSelector(monitorListSelector);
useMappingCheck(monitorList.error);

useEffect(() => {
dispatch(
Expand Down
53 changes: 53 additions & 0 deletions x-pack/plugins/uptime/public/hooks/use_mapping_check.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 43 additions & 0 deletions x-pack/plugins/uptime/public/hooks/use_mapping_check.ts
Original file line number Diff line number Diff line change
@@ -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]);
}
1 change: 1 addition & 0 deletions x-pack/plugins/uptime/public/hooks/use_telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { API_URLS } from '../../common/constants';

export enum UptimePage {
Overview = 'Overview',
MappingError = 'MappingError',
Monitor = 'Monitor',
Settings = 'Settings',
Certificates = 'Certificates',
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/uptime/public/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
78 changes: 78 additions & 0 deletions x-pack/plugins/uptime/public/pages/mapping_error.tsx
Original file line number Diff line number Diff line change
@@ -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>
}
/>
);
};
23 changes: 22 additions & 1 deletion x-pack/plugins/uptime/public/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'>> = ({
Expand Down
39 changes: 24 additions & 15 deletions x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,37 @@ 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;

const decodedPagination = pagination
? 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;
}
},
});
13 changes: 13 additions & 0 deletions x-pack/test/functional/apps/uptime/certificates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 });
});
Expand Down
4 changes: 4 additions & 0 deletions x-pack/test/functional/apps/uptime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 26 additions & 0 deletions x-pack/test/functional/apps/uptime/missing_mappings.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
};
Loading

0 comments on commit 26d19e7

Please sign in to comment.