Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Uptime] Redirect to error page when Heartbeat mappings are missing #110857

Merged
merged 19 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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