mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* WIP: Adding in new reporting diag tool * WIP: chrome-binary test + log capturing/error handling * More wip on diagnostic tool * More work adding in diagnose routes * Alter link in description + minor rename of chrome => browser * Wiring UI to API + some polish on UI flow * WIP: Add in screenshot diag route * Adding in screenshot diag route, hooking up client to it * Add missing lib check + memory check * Working screenshot test + config check for RAM * Small test helper consolidation + screenshot diag test * Delete old i18n translations * PR feedback, browser tests, rename, re-organize import statements and lite fixes * Lite rename for consistency * Remove old validate check i18n * Add config check * i18n all the things! * Docs on diagnostics tool * Fixes, better readability, spelling and more for diagnostic tool * Translate a few error messages * Rename of test => start_logs for clarity. Move to observables * Gathering logs even during process exit or crash * Adds a test case for the browser exiting during the diag check * Tap into browser logs for checking output * Rename asciidoc diag id * Remove duplicate shared object message * Add better comment as to why we merge events + wait for a period of time * Cloning logger for mirroring browser stderr to kibana output Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
9d69007b82
commit
94c13f610f
32 changed files with 1295 additions and 253 deletions
|
@ -7,6 +7,7 @@
|
|||
|
||||
Having trouble? Here are solutions to common problems you might encounter while using Reporting.
|
||||
|
||||
* <<reporting-diagnostics>>
|
||||
* <<reporting-troubleshooting-system-dependencies>>
|
||||
* <<reporting-troubleshooting-text-incorrect>>
|
||||
* <<reporting-troubleshooting-missing-data>>
|
||||
|
@ -15,6 +16,11 @@ Having trouble? Here are solutions to common problems you might encounter while
|
|||
* <<reporting-troubleshooting-puppeteer-debug-logs>>
|
||||
* <<reporting-troubleshooting-system-requirements>>
|
||||
|
||||
[float]
|
||||
[[reporting-diagnostics]]
|
||||
=== Reporting Diagnostics
|
||||
Reporting comes with a built-in utility to try to automatically find common issues. When Kibana is running, navigate to the Report Listing page, and click the "Run reporting diagnostics..." button. This will open up a diagnostic tool that checks various parts of the Kibana deployment to come up with any relevant recommendations.
|
||||
|
||||
[float]
|
||||
[[reporting-troubleshooting-system-dependencies]]
|
||||
=== System dependencies
|
||||
|
|
|
@ -16,6 +16,7 @@ export const API_BASE_URL_V1 = '/api/reporting/v1'; //
|
|||
export const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`;
|
||||
export const API_LIST_URL = '/api/reporting/jobs';
|
||||
export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`;
|
||||
export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`;
|
||||
|
||||
export const CONTENT_TYPE_CSV = 'text/csv';
|
||||
export const CSV_REPORTING_ACTION = 'downloadCsvReport';
|
||||
|
|
281
x-pack/plugins/reporting/public/components/report_diagnostic.tsx
Normal file
281
x-pack/plugins/reporting/public/components/report_diagnostic.tsx
Normal file
|
@ -0,0 +1,281 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiCodeBlock,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiSpacer,
|
||||
EuiSteps,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { ReportingAPIClient, DiagnoseResponse } from '../lib/reporting_api_client';
|
||||
|
||||
interface Props {
|
||||
apiClient: ReportingAPIClient;
|
||||
}
|
||||
|
||||
type ResultStatus = 'danger' | 'incomplete' | 'complete';
|
||||
|
||||
enum statuses {
|
||||
configStatus = 'configStatus',
|
||||
chromeStatus = 'chromeStatus',
|
||||
screenshotStatus = 'screenshotStatus',
|
||||
}
|
||||
|
||||
interface State {
|
||||
isFlyoutVisible: boolean;
|
||||
configStatus: ResultStatus;
|
||||
chromeStatus: ResultStatus;
|
||||
screenshotStatus: ResultStatus;
|
||||
help: string[];
|
||||
logs: string;
|
||||
isBusy: boolean;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
[statuses.configStatus]: 'incomplete',
|
||||
[statuses.chromeStatus]: 'incomplete',
|
||||
[statuses.screenshotStatus]: 'incomplete',
|
||||
isFlyoutVisible: false,
|
||||
help: [],
|
||||
logs: '',
|
||||
isBusy: false,
|
||||
success: true,
|
||||
};
|
||||
|
||||
export const ReportDiagnostic = ({ apiClient }: Props) => {
|
||||
const [state, setStateBase] = useState(initialState);
|
||||
const setState = (s: Partial<typeof state>) =>
|
||||
setStateBase({
|
||||
...state,
|
||||
...s,
|
||||
});
|
||||
const {
|
||||
configStatus,
|
||||
isBusy,
|
||||
screenshotStatus,
|
||||
chromeStatus,
|
||||
isFlyoutVisible,
|
||||
help,
|
||||
logs,
|
||||
success,
|
||||
} = state;
|
||||
|
||||
const closeFlyout = () => setState({ ...initialState, isFlyoutVisible: false });
|
||||
const showFlyout = () => setState({ isFlyoutVisible: true });
|
||||
const apiWrapper = (apiMethod: () => Promise<DiagnoseResponse>, statusProp: statuses) => () => {
|
||||
setState({ isBusy: true, [statusProp]: 'incomplete' });
|
||||
apiMethod()
|
||||
.then((response) => {
|
||||
setState({
|
||||
isBusy: false,
|
||||
help: response.help,
|
||||
logs: response.logs,
|
||||
success: response.success,
|
||||
[statusProp]: response.success ? 'complete' : 'danger',
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setState({
|
||||
isBusy: false,
|
||||
help: [
|
||||
i18n.translate('xpack.reporting.listing.diagnosticApiCallFailure', {
|
||||
defaultMessage: `There was a problem running the diagnostic: {error}`,
|
||||
values: { error },
|
||||
}),
|
||||
],
|
||||
logs: `${error.message}`,
|
||||
success: false,
|
||||
[statusProp]: 'danger',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: i18n.translate('xpack.reporting.listing.diagnosticConfigTitle', {
|
||||
defaultMessage: 'Verify Kibana Configuration',
|
||||
}),
|
||||
children: (
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticConfigMessage"
|
||||
defaultMessage="This check ensures your Kibana configuration is setup properly for reports."
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
disabled={isBusy || configStatus === 'complete'}
|
||||
isLoading={isBusy && configStatus === 'incomplete'}
|
||||
onClick={apiWrapper(apiClient.verifyConfig, statuses.configStatus)}
|
||||
iconType={configStatus === 'complete' ? 'check' : undefined}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticConfigButton"
|
||||
defaultMessage="Verify Configuration"
|
||||
/>
|
||||
</EuiButton>
|
||||
</Fragment>
|
||||
),
|
||||
status: !success && configStatus !== 'complete' ? 'danger' : configStatus,
|
||||
},
|
||||
];
|
||||
|
||||
if (configStatus === 'complete') {
|
||||
steps.push({
|
||||
title: i18n.translate('xpack.reporting.listing.diagnosticBrowserTitle', {
|
||||
defaultMessage: 'Check Browser',
|
||||
}),
|
||||
children: (
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticBrowserMessage"
|
||||
defaultMessage="Reporting utilizes a headless browser to generate PDF and PNGS. This check validates
|
||||
that the browser can launch successfully."
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
disabled={isBusy || chromeStatus === 'complete'}
|
||||
onClick={apiWrapper(apiClient.verifyBrowser, statuses.chromeStatus)}
|
||||
isLoading={isBusy && chromeStatus === 'incomplete'}
|
||||
iconType={chromeStatus === 'complete' ? 'check' : undefined}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticBrowserButton"
|
||||
defaultMessage="Check Browser"
|
||||
/>
|
||||
</EuiButton>
|
||||
</Fragment>
|
||||
),
|
||||
status: !success && chromeStatus !== 'complete' ? 'danger' : chromeStatus,
|
||||
});
|
||||
}
|
||||
|
||||
if (chromeStatus === 'complete') {
|
||||
steps.push({
|
||||
title: i18n.translate('xpack.reporting.listing.diagnosticScreenshotTitle', {
|
||||
defaultMessage: 'Check Screen Capture Capabilities',
|
||||
}),
|
||||
children: (
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticScreenshotMessage"
|
||||
defaultMessage="This check ensures the headless browser can capture a screenshot of a page."
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
disabled={isBusy || screenshotStatus === 'complete'}
|
||||
onClick={apiWrapper(apiClient.verifyScreenCapture, statuses.screenshotStatus)}
|
||||
isLoading={isBusy && screenshotStatus === 'incomplete'}
|
||||
iconType={screenshotStatus === 'complete' ? 'check' : undefined}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticScreenshotButton"
|
||||
defaultMessage="Capture Screenshot"
|
||||
/>
|
||||
</EuiButton>
|
||||
</Fragment>
|
||||
),
|
||||
status: !success && screenshotStatus !== 'complete' ? 'danger' : screenshotStatus,
|
||||
});
|
||||
}
|
||||
|
||||
if (screenshotStatus === 'complete') {
|
||||
steps.push({
|
||||
title: i18n.translate('xpack.reporting.listing.diagnosticSuccessTitle', {
|
||||
defaultMessage: 'All set!',
|
||||
}),
|
||||
children: (
|
||||
<Fragment>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticSuccessMessage"
|
||||
defaultMessage="Excellent! Everything looks like shipshape for reporting to function!"
|
||||
/>
|
||||
</Fragment>
|
||||
),
|
||||
status: !success ? 'danger' : screenshotStatus,
|
||||
});
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
steps.push({
|
||||
title: i18n.translate('xpack.reporting.listing.diagnosticFailureTitle', {
|
||||
defaultMessage: "Whoops! Looks like something isn't working properly.",
|
||||
}),
|
||||
children: (
|
||||
<Fragment>
|
||||
{help.length ? (
|
||||
<Fragment>
|
||||
<EuiCallOut color="danger" iconType="alert">
|
||||
<p>{help.join('\n')}</p>
|
||||
</EuiCallOut>
|
||||
</Fragment>
|
||||
) : null}
|
||||
{logs.length ? (
|
||||
<Fragment>
|
||||
<EuiSpacer />
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticFailureDescription"
|
||||
defaultMessage="Here are some more details about the issue:"
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiCodeBlock>{logs}</EuiCodeBlock>
|
||||
</Fragment>
|
||||
) : null}
|
||||
</Fragment>
|
||||
),
|
||||
status: 'danger',
|
||||
});
|
||||
}
|
||||
|
||||
let flyout;
|
||||
if (isFlyoutVisible) {
|
||||
flyout = (
|
||||
<EuiFlyout onClose={closeFlyout} aria-labelledby="reportingHelperTitle" size="m">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticTitle"
|
||||
defaultMessage="Reporting Diagnostics"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticDescription"
|
||||
defaultMessage="Automatically run a series of diagnostics to troubleshoot common reporting problems."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiSteps steps={steps} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{flyout}
|
||||
<EuiButtonEmpty size="xs" flush="left" onClick={showFlyout}>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.diagnosticButton"
|
||||
defaultMessage="Run reporting diagnostics..."
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
|
@ -31,6 +33,7 @@ import {
|
|||
ReportErrorButton,
|
||||
ReportInfoButton,
|
||||
} from './buttons';
|
||||
import { ReportDiagnostic } from './report_diagnostic';
|
||||
|
||||
export interface Job {
|
||||
id: string;
|
||||
|
@ -134,23 +137,38 @@ class ReportListingUi extends Component<Props, State> {
|
|||
|
||||
public render() {
|
||||
return (
|
||||
<EuiPageContent horizontalPosition="center" className="euiPageBody--restrictWidth-default">
|
||||
<EuiTitle>
|
||||
<h1>
|
||||
<FormattedMessage id="xpack.reporting.listing.reportstitle" defaultMessage="Reports" />
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.reports.subtitle"
|
||||
defaultMessage="Find reports generated in Kibana applications here"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
{this.renderTable()}
|
||||
</EuiPageContent>
|
||||
<div>
|
||||
<EuiPageContent horizontalPosition="center" className="euiPageBody--restrictWidth-default">
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.reportstitle"
|
||||
defaultMessage="Reports"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.reporting.listing.reports.subtitle"
|
||||
defaultMessage="Find reports generated in Kibana applications here"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{this.renderTable()}
|
||||
</EuiPageContent>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween" direction="rowReverse">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ReportDiagnostic apiClient={this.props.apiClient} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,12 @@ import { stringify } from 'query-string';
|
|||
import rison from 'rison-node';
|
||||
import { HttpSetup } from 'src/core/public';
|
||||
import { JobId, SourceJob } from '../../common/types';
|
||||
import { API_BASE_GENERATE, API_LIST_URL, REPORTING_MANAGEMENT_HOME } from '../../constants';
|
||||
import {
|
||||
API_BASE_URL,
|
||||
API_BASE_GENERATE,
|
||||
API_LIST_URL,
|
||||
REPORTING_MANAGEMENT_HOME,
|
||||
} from '../../constants';
|
||||
import { add } from './job_completion_notifications';
|
||||
|
||||
export interface JobQueueEntry {
|
||||
|
@ -59,6 +64,12 @@ interface JobParams {
|
|||
[paramName: string]: any;
|
||||
}
|
||||
|
||||
export interface DiagnoseResponse {
|
||||
help: string[];
|
||||
success: boolean;
|
||||
logs: string;
|
||||
}
|
||||
|
||||
export class ReportingAPIClient {
|
||||
private http: HttpSetup;
|
||||
|
||||
|
@ -157,4 +168,28 @@ export class ReportingAPIClient {
|
|||
* provides the raw server basePath to allow it to be stripped out from relativeUrls in job params
|
||||
*/
|
||||
public getServerBasePath = () => this.http.basePath.serverBasePath;
|
||||
|
||||
/*
|
||||
* Diagnostic-related API calls
|
||||
*/
|
||||
public verifyConfig = (): Promise<DiagnoseResponse> =>
|
||||
this.http.post(`${API_BASE_URL}/diagnose/config`, {
|
||||
asSystemRequest: true,
|
||||
});
|
||||
|
||||
/*
|
||||
* Diagnostic-related API calls
|
||||
*/
|
||||
public verifyBrowser = (): Promise<DiagnoseResponse> =>
|
||||
this.http.post(`${API_BASE_URL}/diagnose/browser`, {
|
||||
asSystemRequest: true,
|
||||
});
|
||||
|
||||
/*
|
||||
* Diagnostic-related API calls
|
||||
*/
|
||||
public verifyScreenCapture = (): Promise<DiagnoseResponse> =>
|
||||
this.http.post(`${API_BASE_URL}/diagnose/screenshot`, {
|
||||
asSystemRequest: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -59,28 +59,6 @@ export class HeadlessChromiumDriverFactory {
|
|||
|
||||
type = BROWSER_TYPE;
|
||||
|
||||
test(logger: LevelLogger) {
|
||||
const chromiumArgs = args({
|
||||
userDataDir: this.userDataDir,
|
||||
viewport: { width: 800, height: 600 },
|
||||
disableSandbox: this.browserConfig.disableSandbox,
|
||||
proxy: this.browserConfig.proxy,
|
||||
});
|
||||
|
||||
return puppeteerLaunch({
|
||||
userDataDir: this.userDataDir,
|
||||
executablePath: this.binaryPath,
|
||||
ignoreHTTPSErrors: true,
|
||||
args: chromiumArgs,
|
||||
} as LaunchOptions).catch((error: Error) => {
|
||||
logger.error(
|
||||
`The Reporting plugin encountered issues launching Chromium in a self-test. You may have trouble generating reports.`
|
||||
);
|
||||
logger.error(error);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Return an observable to objects which will drive screenshot capture for a page
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { spawn } from 'child_process';
|
||||
import del from 'del';
|
||||
import { mkdtempSync } from 'fs';
|
||||
import { uniq } from 'lodash';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { createInterface } from 'readline';
|
||||
import { fromEvent, timer, merge, of } from 'rxjs';
|
||||
import { takeUntil, map, reduce, tap, catchError } from 'rxjs/operators';
|
||||
import { ReportingCore } from '../../..';
|
||||
import { LevelLogger } from '../../../lib';
|
||||
import { getBinaryPath } from '../../install';
|
||||
import { args } from './args';
|
||||
|
||||
const browserLaunchTimeToWait = 5 * 1000;
|
||||
|
||||
// Default args used by pptr
|
||||
// https://github.com/puppeteer/puppeteer/blob/13ea347/src/node/Launcher.ts#L168
|
||||
const defaultArgs = [
|
||||
'--disable-background-networking',
|
||||
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-breakpad',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
'--disable-features=TranslateUI',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-sync',
|
||||
'--force-color-profile=srgb',
|
||||
'--metrics-recording-only',
|
||||
'--no-first-run',
|
||||
'--enable-automation',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--remote-debugging-port=0',
|
||||
'--headless',
|
||||
];
|
||||
|
||||
export const browserStartLogs = (
|
||||
core: ReportingCore,
|
||||
logger: LevelLogger,
|
||||
overrideFlags: string[] = []
|
||||
) => {
|
||||
const config = core.getConfig();
|
||||
const proxy = config.get('capture', 'browser', 'chromium', 'proxy');
|
||||
const disableSandbox = config.get('capture', 'browser', 'chromium', 'disableSandbox');
|
||||
const userDataDir = mkdtempSync(join(tmpdir(), 'chromium-'));
|
||||
const binaryPath = getBinaryPath();
|
||||
const kbnArgs = args({
|
||||
userDataDir,
|
||||
viewport: { width: 800, height: 600 },
|
||||
disableSandbox,
|
||||
proxy,
|
||||
});
|
||||
const finalArgs = uniq([...defaultArgs, ...kbnArgs, ...overrideFlags]);
|
||||
|
||||
// On non-windows platforms, `detached: true` makes child process a
|
||||
// leader of a new process group, making it possible to kill child
|
||||
// process tree with `.kill(-pid)` command. @see
|
||||
// https://nodejs.org/api/child_process.html#child_process_options_detached
|
||||
const browserProcess = spawn(binaryPath, finalArgs, {
|
||||
detached: process.platform !== 'win32',
|
||||
});
|
||||
|
||||
const rl = createInterface({ input: browserProcess.stderr });
|
||||
|
||||
const exit$ = fromEvent(browserProcess, 'exit').pipe(
|
||||
map((code) => {
|
||||
logger.error(`Browser exited abnormally, received code: ${code}`);
|
||||
return i18n.translate('xpack.reporting.diagnostic.browserCrashed', {
|
||||
defaultMessage: `Browser exited abnormally during startup`,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const error$ = fromEvent(browserProcess, 'error').pipe(
|
||||
map(() => {
|
||||
logger.error(`Browser process threw an error on startup`);
|
||||
return i18n.translate('xpack.reporting.diagnostic.browserErrored', {
|
||||
defaultMessage: `Browser process threw an error on startup`,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const browserProcessLogger = logger.clone(['chromium-stderr']);
|
||||
const log$ = fromEvent(rl, 'line').pipe(
|
||||
tap((message: unknown) => {
|
||||
if (typeof message === 'string') {
|
||||
browserProcessLogger.info(message);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Collect all events (exit, error and on log-lines), but let chromium keep spitting out
|
||||
// logs as sometimes it's "bind" successfully for remote connections, but later emit
|
||||
// a log indicative of an issue (for example, no default font found).
|
||||
return merge(exit$, error$, log$).pipe(
|
||||
takeUntil(timer(browserLaunchTimeToWait)),
|
||||
reduce((acc, curr) => `${acc}${curr}\n`, ''),
|
||||
tap(() => {
|
||||
if (browserProcess && browserProcess.pid && !browserProcess.killed) {
|
||||
browserProcess.kill('SIGKILL');
|
||||
logger.info(`Successfully sent 'SIGKILL' to browser process (PID: ${browserProcess.pid})`);
|
||||
}
|
||||
browserProcess.removeAllListeners();
|
||||
rl.removeAllListeners();
|
||||
rl.close();
|
||||
del(userDataDir, { force: true }).catch((error) => {
|
||||
logger.error(`Error deleting user data directory at [${userDataDir}]!`);
|
||||
logger.error(error);
|
||||
});
|
||||
}),
|
||||
catchError((error) => {
|
||||
logger.error(error);
|
||||
return of(error);
|
||||
})
|
||||
);
|
||||
};
|
|
@ -4,24 +4,43 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import del from 'del';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import del from 'del';
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { LevelLogger } from '../lib';
|
||||
import { paths } from './chromium/paths';
|
||||
import { ensureBrowserDownloaded } from './download';
|
||||
// @ts-ignore
|
||||
import { md5 } from './download/checksum';
|
||||
// @ts-ignore
|
||||
import { extract } from './extract';
|
||||
import { paths } from './chromium/paths';
|
||||
|
||||
interface Package {
|
||||
platforms: string[];
|
||||
architecture: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Small helper util to resolve where chromium is installed
|
||||
*/
|
||||
export const getBinaryPath = (
|
||||
chromiumPath: string = path.resolve(__dirname, '../../chromium'),
|
||||
platform: string = process.platform,
|
||||
architecture: string = os.arch()
|
||||
) => {
|
||||
const pkg = paths.packages.find((p: Package) => {
|
||||
return p.platforms.includes(platform) && p.architecture === architecture;
|
||||
});
|
||||
|
||||
if (!pkg) {
|
||||
// TODO: validate this
|
||||
throw new Error(`Unsupported platform: ${platform}-${architecture}`);
|
||||
}
|
||||
|
||||
return path.join(chromiumPath, pkg.binaryRelativePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* "install" a browser by type into installs path by extracting the downloaded
|
||||
* archive. If there is an error extracting the archive an `ExtractError` is thrown
|
||||
|
@ -43,7 +62,7 @@ export function installBrowser(
|
|||
throw new Error(`Unsupported platform: ${platform}-${architecture}`);
|
||||
}
|
||||
|
||||
const binaryPath = path.join(chromiumPath, pkg.binaryRelativePath);
|
||||
const binaryPath = getBinaryPath(chromiumPath, platform, architecture);
|
||||
const binaryChecksum = await md5(binaryPath).catch(() => '');
|
||||
|
||||
if (binaryChecksum !== pkg.binaryChecksum) {
|
||||
|
|
|
@ -28,7 +28,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
|
|||
if (!layoutParams || !layoutParams.dimensions) {
|
||||
throw new Error(`LayoutParams.Dimensions is undefined.`);
|
||||
}
|
||||
const layout = new PreserveLayout(layoutParams.dimensions);
|
||||
const layout = new PreserveLayout(layoutParams.dimensions, layoutParams.selectors);
|
||||
if (apmLayout) apmLayout.end();
|
||||
|
||||
const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup');
|
||||
|
|
|
@ -13,4 +13,3 @@ export { LevelLogger } from './level_logger';
|
|||
export { statuses } from './statuses';
|
||||
export { ReportingStore } from './store';
|
||||
export { startTrace } from './trace';
|
||||
export { runValidations } from './validate';
|
||||
|
|
|
@ -54,6 +54,7 @@ export interface Size {
|
|||
export interface LayoutParams {
|
||||
id: string;
|
||||
dimensions: Size;
|
||||
selectors?: LayoutSelectorDictionary;
|
||||
}
|
||||
|
||||
interface LayoutSelectors {
|
||||
|
|
|
@ -25,12 +25,16 @@ export class PreserveLayout extends Layout {
|
|||
private readonly scaledHeight: number;
|
||||
private readonly scaledWidth: number;
|
||||
|
||||
constructor(size: Size) {
|
||||
constructor(size: Size, layoutSelectors?: LayoutSelectorDictionary) {
|
||||
super(LayoutTypes.PRESERVE_LAYOUT);
|
||||
this.height = size.height;
|
||||
this.width = size.width;
|
||||
this.scaledHeight = size.height * ZOOM;
|
||||
this.scaledWidth = size.width * ZOOM;
|
||||
|
||||
if (layoutSelectors) {
|
||||
this.selectors = layoutSelectors;
|
||||
}
|
||||
}
|
||||
|
||||
public getCssOverridesPath() {
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
import sinon from 'sinon';
|
||||
import { ElasticsearchServiceSetup } from 'src/core/server';
|
||||
import { ReportingConfig, ReportingCore } from '../..';
|
||||
import { createMockReportingCore } from '../../test_helpers';
|
||||
import { createMockLevelLogger } from '../../test_helpers/create_mock_levellogger';
|
||||
import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers';
|
||||
import { Report } from './report';
|
||||
import { ReportingStore } from './store';
|
||||
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ElasticsearchServiceSetup } from 'kibana/server';
|
||||
import { ReportingConfig } from '../../';
|
||||
import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory';
|
||||
import { LevelLogger } from '../';
|
||||
import { validateBrowser } from './validate_browser';
|
||||
import { validateMaxContentLength } from './validate_max_content_length';
|
||||
|
||||
export async function runValidations(
|
||||
config: ReportingConfig,
|
||||
elasticsearch: ElasticsearchServiceSetup,
|
||||
browserFactory: HeadlessChromiumDriverFactory,
|
||||
parentLogger: LevelLogger
|
||||
) {
|
||||
const logger = parentLogger.clone(['validations']);
|
||||
try {
|
||||
await Promise.all([
|
||||
validateBrowser(browserFactory, logger),
|
||||
validateMaxContentLength(config, elasticsearch, logger),
|
||||
]);
|
||||
logger.debug(
|
||||
i18n.translate('xpack.reporting.selfCheck.ok', {
|
||||
defaultMessage: `Reporting plugin self-check ok!`,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
logger.warning(
|
||||
i18n.translate('xpack.reporting.selfCheck.warning', {
|
||||
defaultMessage: `Reporting plugin self-check generated a warning: {err}`,
|
||||
values: {
|
||||
err,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Browser } from 'puppeteer';
|
||||
import { BROWSER_TYPE } from '../../../common/constants';
|
||||
import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory';
|
||||
import { LevelLogger } from '../';
|
||||
|
||||
/*
|
||||
* Validate the Reporting headless browser can launch, and that it can connect
|
||||
* to the locally running Kibana instance.
|
||||
*/
|
||||
export const validateBrowser = async (
|
||||
browserFactory: HeadlessChromiumDriverFactory,
|
||||
logger: LevelLogger
|
||||
) => {
|
||||
if (browserFactory.type === BROWSER_TYPE) {
|
||||
return browserFactory.test(logger).then((browser: Browser | null) => {
|
||||
if (browser && browser.close) {
|
||||
browser.close();
|
||||
} else {
|
||||
throw new Error('Could not close browser client handle!');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import sinon from 'sinon';
|
||||
import { validateMaxContentLength } from './validate_max_content_length';
|
||||
|
||||
const FIVE_HUNDRED_MEGABYTES = 524288000;
|
||||
const ONE_HUNDRED_MEGABYTES = 104857600;
|
||||
|
||||
describe('Reporting: Validate Max Content Length', () => {
|
||||
const elasticsearch = {
|
||||
legacy: {
|
||||
client: {
|
||||
callAsInternalUser: () => ({
|
||||
defaults: {
|
||||
http: {
|
||||
max_content_length: '100mb',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const logger = {
|
||||
warning: sinon.spy(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
logger.warning.resetHistory();
|
||||
});
|
||||
|
||||
it('should log warning messages when reporting has a higher max-size than elasticsearch', async () => {
|
||||
const config = { get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES) };
|
||||
const elasticsearch = {
|
||||
legacy: {
|
||||
client: {
|
||||
callAsInternalUser: () => ({
|
||||
defaults: {
|
||||
http: {
|
||||
max_content_length: '100mb',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await validateMaxContentLength(config, elasticsearch, logger);
|
||||
|
||||
sinon.assert.calledWithMatch(
|
||||
logger.warning,
|
||||
`xpack.reporting.csv.maxSizeBytes (524288000) is higher`
|
||||
);
|
||||
sinon.assert.calledWithMatch(
|
||||
logger.warning,
|
||||
`than ElasticSearch's http.max_content_length (104857600)`
|
||||
);
|
||||
sinon.assert.calledWithMatch(
|
||||
logger.warning,
|
||||
'Please set http.max_content_length in ElasticSearch to match'
|
||||
);
|
||||
sinon.assert.calledWithMatch(
|
||||
logger.warning,
|
||||
'or lower your xpack.reporting.csv.maxSizeBytes in Kibana'
|
||||
);
|
||||
});
|
||||
|
||||
it('should do nothing when reporting has the same max-size as elasticsearch', async () => {
|
||||
const config = { get: sinon.stub().returns(ONE_HUNDRED_MEGABYTES) };
|
||||
|
||||
expect(
|
||||
async () => await validateMaxContentLength(config, elasticsearch, logger.warning)
|
||||
).not.toThrow();
|
||||
sinon.assert.notCalled(logger.warning);
|
||||
});
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import numeral from '@elastic/numeral';
|
||||
import { ElasticsearchServiceSetup } from 'kibana/server';
|
||||
import { defaults, get } from 'lodash';
|
||||
import { ReportingConfig } from '../../';
|
||||
import { LevelLogger } from '../';
|
||||
|
||||
const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes';
|
||||
const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length';
|
||||
|
||||
export async function validateMaxContentLength(
|
||||
config: ReportingConfig,
|
||||
elasticsearch: ElasticsearchServiceSetup,
|
||||
logger: LevelLogger
|
||||
) {
|
||||
const { callAsInternalUser } = elasticsearch.legacy.client;
|
||||
|
||||
const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', {
|
||||
includeDefaults: true,
|
||||
});
|
||||
const { persistent, transient, defaults: defaultSettings } = elasticClusterSettingsResponse;
|
||||
const elasticClusterSettings = defaults({}, persistent, transient, defaultSettings);
|
||||
|
||||
const elasticSearchMaxContent = get(elasticClusterSettings, 'http.max_content_length', '100mb');
|
||||
const elasticSearchMaxContentBytes = numeral().unformat(elasticSearchMaxContent.toUpperCase());
|
||||
const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes');
|
||||
|
||||
if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) {
|
||||
// TODO this should simply throw an error and let the handler conver it to a warning mesasge. See validateServerHost.
|
||||
logger.warning(
|
||||
`xpack.reporting.${KIBANA_MAX_SIZE_BYTES_PATH} (${kibanaMaxContentBytes}) is higher than ElasticSearch's ${ES_MAX_SIZE_BYTES_PATH} (${elasticSearchMaxContentBytes}). ` +
|
||||
`Please set ${ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.${KIBANA_MAX_SIZE_BYTES_PATH} in Kibana to avoid this warning.`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from '../common/constants';
|
|||
import { ReportingCore } from './';
|
||||
import { initializeBrowserDriverFactory } from './browsers';
|
||||
import { buildConfig, ReportingConfigType } from './config';
|
||||
import { createQueueFactory, LevelLogger, ReportingStore, runValidations } from './lib';
|
||||
import { createQueueFactory, LevelLogger, ReportingStore } from './lib';
|
||||
import { registerRoutes } from './routes';
|
||||
import { setFieldFormats } from './services';
|
||||
import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types';
|
||||
|
@ -105,7 +105,6 @@ export class ReportingPlugin
|
|||
setFieldFormats(plugins.data.fieldFormats);
|
||||
|
||||
const { logger, reportingCore } = this;
|
||||
const { elasticsearch } = reportingCore.getPluginSetupDeps();
|
||||
|
||||
// async background start
|
||||
(async () => {
|
||||
|
@ -124,9 +123,6 @@ export class ReportingPlugin
|
|||
store,
|
||||
});
|
||||
|
||||
// run self-check validations
|
||||
runValidations(config, elasticsearch, browserDriverFactory, this.logger);
|
||||
|
||||
this.logger.debug('Start complete');
|
||||
})().catch((e) => {
|
||||
this.logger.error(`Error in Reporting start, reporting may not function properly`);
|
||||
|
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UnwrapPromise } from '@kbn/utility-types';
|
||||
import { spawn } from 'child_process';
|
||||
import { createInterface } from 'readline';
|
||||
import { setupServer } from 'src/core/server/test_utils';
|
||||
import supertest from 'supertest';
|
||||
import { ReportingCore } from '../..';
|
||||
import { createMockLevelLogger, createMockReportingCore } from '../../test_helpers';
|
||||
import { registerDiagnoseBrowser } from './browser';
|
||||
|
||||
jest.mock('child_process');
|
||||
jest.mock('readline');
|
||||
|
||||
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
||||
|
||||
const devtoolMessage = 'DevTools listening on (ws://localhost:4000)';
|
||||
const fontNotFoundMessage = 'Could not find the default font';
|
||||
|
||||
describe('POST /diagnose/browser', () => {
|
||||
jest.setTimeout(6000);
|
||||
const reportingSymbol = Symbol('reporting');
|
||||
const mockLogger = createMockLevelLogger();
|
||||
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
let core: ReportingCore;
|
||||
const mockedSpawn: any = spawn;
|
||||
const mockedCreateInterface: any = createInterface;
|
||||
|
||||
const config = {
|
||||
get: jest.fn().mockImplementation(() => ({})),
|
||||
kbnConfig: { get: jest.fn() },
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, httpSetup } = await setupServer(reportingSymbol));
|
||||
httpSetup.registerRouteHandlerContext(reportingSymbol, 'reporting', () => ({}));
|
||||
|
||||
const mockSetupDeps = ({
|
||||
elasticsearch: {
|
||||
legacy: { client: { callAsInternalUser: jest.fn() } },
|
||||
},
|
||||
router: httpSetup.createRouter(''),
|
||||
} as unknown) as any;
|
||||
|
||||
core = await createMockReportingCore(config, mockSetupDeps);
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: jest.fn(),
|
||||
kill: jest.fn(),
|
||||
pid: 123,
|
||||
stderr: 'stderr',
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('returns a 200 when successful', async () => {
|
||||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0),
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body.success).toEqual(true);
|
||||
expect(body.help).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns logs when browser crashes + helpful links', async () => {
|
||||
const logs = `Could not find the default font`;
|
||||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (e: string, cb: any) => setTimeout(() => cb(logs), 0),
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: jest.fn(),
|
||||
kill: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"help": Array [
|
||||
"The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.",
|
||||
],
|
||||
"logs": "Could not find the default font
|
||||
",
|
||||
"success": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('logs a message when the browser starts, but then has problems later', async () => {
|
||||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (e: string, cb: any) => {
|
||||
setTimeout(() => cb(devtoolMessage), 0);
|
||||
setTimeout(() => cb(fontNotFoundMessage), 0);
|
||||
},
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: jest.fn(),
|
||||
kill: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"help": Array [
|
||||
"The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.",
|
||||
],
|
||||
"logs": "DevTools listening on (ws://localhost:4000)
|
||||
Could not find the default font
|
||||
",
|
||||
"success": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('logs a message when the browser starts, but then crashes', async () => {
|
||||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (e: string, cb: any) => {
|
||||
setTimeout(() => cb(fontNotFoundMessage), 0);
|
||||
},
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: jest.fn(),
|
||||
kill: jest.fn(),
|
||||
addEventListener: (e: string, cb: any) => {
|
||||
if (e === 'exit') {
|
||||
setTimeout(() => cb(), 5);
|
||||
}
|
||||
},
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"help": Array [
|
||||
"The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.",
|
||||
],
|
||||
"logs": "Could not find the default font
|
||||
Browser exited abnormally during startup
|
||||
",
|
||||
"success": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('cleans up process and subscribers', async () => {
|
||||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
const killMock = jest.fn();
|
||||
const spawnListenersMock = jest.fn();
|
||||
const createInterfaceListenersMock = jest.fn();
|
||||
const createInterfaceCloseMock = jest.fn();
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: spawnListenersMock,
|
||||
kill: killMock,
|
||||
pid: 123,
|
||||
stderr: 'stderr',
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0),
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: createInterfaceListenersMock,
|
||||
close: createInterfaceCloseMock,
|
||||
}));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
.expect(200)
|
||||
.then(() => {
|
||||
expect(killMock.mock.calls.length).toBe(1);
|
||||
expect(spawnListenersMock.mock.calls.length).toBe(1);
|
||||
expect(createInterfaceListenersMock.mock.calls.length).toBe(1);
|
||||
expect(createInterfaceCloseMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
78
x-pack/plugins/reporting/server/routes/diagnostic/browser.ts
Normal file
78
x-pack/plugins/reporting/server/routes/diagnostic/browser.ts
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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ReportingCore } from '../..';
|
||||
import { API_DIAGNOSE_URL } from '../../../common/constants';
|
||||
import { browserStartLogs } from '../../browsers/chromium/driver_factory/start_logs';
|
||||
import { LevelLogger as Logger } from '../../lib';
|
||||
import { DiagnosticResponse } from '../../types';
|
||||
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
|
||||
|
||||
const logsToHelpMap = {
|
||||
'error while loading shared libraries': i18n.translate(
|
||||
'xpack.reporting.diagnostic.browserMissingDependency',
|
||||
{
|
||||
defaultMessage: `The browser couldn't start properly due to missing system dependencies. Please see {url}`,
|
||||
values: {
|
||||
url:
|
||||
'https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies',
|
||||
},
|
||||
}
|
||||
),
|
||||
|
||||
'Could not find the default font': i18n.translate(
|
||||
'xpack.reporting.diagnostic.browserMissingFonts',
|
||||
{
|
||||
defaultMessage: `The browser couldn't locate a default font. Please see {url} to fix this issue.`,
|
||||
values: {
|
||||
url:
|
||||
'https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies',
|
||||
},
|
||||
}
|
||||
),
|
||||
|
||||
'No usable sandbox': i18n.translate('xpack.reporting.diagnostic.noUsableSandbox', {
|
||||
defaultMessage: `Unable to use Chromium sandbox. This can be disabled at your own risk with 'xpack.reporting.capture.browser.chromium.disableSandbox'. Please see {url}`,
|
||||
values: {
|
||||
url:
|
||||
'https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-sandbox-dependency',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger) => {
|
||||
const { router } = reporting.getPluginSetupDeps();
|
||||
const userHandler = authorizedUserPreRoutingFactory(reporting);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: `${API_DIAGNOSE_URL}/browser`,
|
||||
validate: {},
|
||||
},
|
||||
userHandler(async (user, context, req, res) => {
|
||||
const logs = await browserStartLogs(reporting, logger).toPromise();
|
||||
const knownIssues = Object.keys(logsToHelpMap) as Array<keyof typeof logsToHelpMap>;
|
||||
|
||||
const boundSuccessfully = logs.includes(`DevTools listening on`);
|
||||
const help = knownIssues.reduce((helpTexts: string[], knownIssue) => {
|
||||
const helpText = logsToHelpMap[knownIssue];
|
||||
if (logs.includes(knownIssue)) {
|
||||
helpTexts.push(helpText);
|
||||
}
|
||||
return helpTexts;
|
||||
}, []);
|
||||
|
||||
const response: DiagnosticResponse = {
|
||||
success: boundSuccessfully && !help.length,
|
||||
help,
|
||||
logs,
|
||||
};
|
||||
|
||||
return res.ok({ body: response });
|
||||
})
|
||||
);
|
||||
};
|
107
x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts
Normal file
107
x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UnwrapPromise } from '@kbn/utility-types';
|
||||
import { setupServer } from 'src/core/server/test_utils';
|
||||
import supertest from 'supertest';
|
||||
import { ReportingCore } from '../..';
|
||||
import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers';
|
||||
import { registerDiagnoseConfig } from './config';
|
||||
|
||||
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
||||
|
||||
describe('POST /diagnose/config', () => {
|
||||
const reportingSymbol = Symbol('reporting');
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
let core: ReportingCore;
|
||||
let mockSetupDeps: any;
|
||||
let config: any;
|
||||
|
||||
const mockLogger = createMockLevelLogger();
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, httpSetup } = await setupServer(reportingSymbol));
|
||||
httpSetup.registerRouteHandlerContext(reportingSymbol, 'reporting', () => ({}));
|
||||
|
||||
mockSetupDeps = ({
|
||||
elasticsearch: {
|
||||
legacy: { client: { callAsInternalUser: jest.fn() } },
|
||||
},
|
||||
router: httpSetup.createRouter(''),
|
||||
} as unknown) as any;
|
||||
|
||||
config = {
|
||||
get: jest.fn(),
|
||||
kbnConfig: { get: jest.fn() },
|
||||
};
|
||||
|
||||
core = await createMockReportingCore(config, mockSetupDeps);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('returns a 200 by default when configured properly', async () => {
|
||||
mockSetupDeps.elasticsearch.legacy.client.callAsInternalUser.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
defaults: {
|
||||
http: {
|
||||
max_content_length: '100mb',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
registerDiagnoseConfig(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/config')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"help": Array [],
|
||||
"logs": "",
|
||||
"success": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 200 with help text when not configured properly', async () => {
|
||||
config.get.mockImplementation(() => 10485760);
|
||||
mockSetupDeps.elasticsearch.legacy.client.callAsInternalUser.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
defaults: {
|
||||
http: {
|
||||
max_content_length: '5mb',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
registerDiagnoseConfig(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/config')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"help": Array [
|
||||
"xpack.reporting.csv.maxSizeBytes (10485760) is higher than ElasticSearch's http.max_content_length (5242880). Please set http.max_content_length in ElasticSearch to match, or lower your xpack.reporting.csv.maxSizeBytes in Kibana.",
|
||||
],
|
||||
"logs": "xpack.reporting.csv.maxSizeBytes (10485760) is higher than ElasticSearch's http.max_content_length (5242880). Please set http.max_content_length in ElasticSearch to match, or lower your xpack.reporting.csv.maxSizeBytes in Kibana.",
|
||||
"success": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
81
x-pack/plugins/reporting/server/routes/diagnostic/config.ts
Normal file
81
x-pack/plugins/reporting/server/routes/diagnostic/config.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { defaults, get } from 'lodash';
|
||||
import { ReportingCore } from '../..';
|
||||
import { API_DIAGNOSE_URL } from '../../../common/constants';
|
||||
import { LevelLogger as Logger } from '../../lib';
|
||||
import { DiagnosticResponse } from '../../types';
|
||||
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
|
||||
|
||||
const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes';
|
||||
const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length';
|
||||
|
||||
export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => {
|
||||
const setupDeps = reporting.getPluginSetupDeps();
|
||||
const userHandler = authorizedUserPreRoutingFactory(reporting);
|
||||
const { router, elasticsearch } = setupDeps;
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: `${API_DIAGNOSE_URL}/config`,
|
||||
validate: {},
|
||||
},
|
||||
userHandler(async (user, context, req, res) => {
|
||||
const warnings = [];
|
||||
const { callAsInternalUser } = elasticsearch.legacy.client;
|
||||
const config = reporting.getConfig();
|
||||
|
||||
const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', {
|
||||
includeDefaults: true,
|
||||
});
|
||||
const { persistent, transient, defaults: defaultSettings } = elasticClusterSettingsResponse;
|
||||
const elasticClusterSettings = defaults({}, persistent, transient, defaultSettings);
|
||||
|
||||
const elasticSearchMaxContent = get(
|
||||
elasticClusterSettings,
|
||||
'http.max_content_length',
|
||||
'100mb'
|
||||
);
|
||||
const elasticSearchMaxContentBytes = numeral().unformat(
|
||||
elasticSearchMaxContent.toUpperCase()
|
||||
);
|
||||
const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes');
|
||||
|
||||
if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) {
|
||||
const maxContentSizeWarning = i18n.translate(
|
||||
'xpack.reporting.diagnostic.configSizeMismatch',
|
||||
{
|
||||
defaultMessage:
|
||||
`xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) is higher than ElasticSearch's {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes}). ` +
|
||||
`Please set {ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} in Kibana.`,
|
||||
values: {
|
||||
kibanaMaxContentBytes,
|
||||
elasticSearchMaxContentBytes,
|
||||
KIBANA_MAX_SIZE_BYTES_PATH,
|
||||
ES_MAX_SIZE_BYTES_PATH,
|
||||
},
|
||||
}
|
||||
);
|
||||
warnings.push(maxContentSizeWarning);
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
warnings.forEach((warn) => logger.warn(warn));
|
||||
}
|
||||
|
||||
const body: DiagnosticResponse = {
|
||||
help: warnings,
|
||||
success: !warnings.length,
|
||||
logs: warnings.join('\n'),
|
||||
};
|
||||
|
||||
return res.ok({ body });
|
||||
})
|
||||
);
|
||||
};
|
17
x-pack/plugins/reporting/server/routes/diagnostic/index.ts
Normal file
17
x-pack/plugins/reporting/server/routes/diagnostic/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { registerDiagnoseBrowser } from './browser';
|
||||
import { registerDiagnoseConfig } from './config';
|
||||
import { registerDiagnoseScreenshot } from './screenshot';
|
||||
import { LevelLogger as Logger } from '../../lib';
|
||||
import { ReportingCore } from '../../core';
|
||||
|
||||
export const registerDiagnosticRoutes = (reporting: ReportingCore, logger: Logger) => {
|
||||
registerDiagnoseBrowser(reporting, logger);
|
||||
registerDiagnoseConfig(reporting, logger);
|
||||
registerDiagnoseScreenshot(reporting, logger);
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UnwrapPromise } from '@kbn/utility-types';
|
||||
import { setupServer } from 'src/core/server/test_utils';
|
||||
import supertest from 'supertest';
|
||||
import { ReportingCore } from '../..';
|
||||
import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers';
|
||||
import { registerDiagnoseScreenshot } from './screenshot';
|
||||
|
||||
jest.mock('../../export_types/png/lib/generate_png');
|
||||
|
||||
import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png';
|
||||
|
||||
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
||||
|
||||
describe('POST /diagnose/screenshot', () => {
|
||||
const reportingSymbol = Symbol('reporting');
|
||||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
let core: ReportingCore;
|
||||
|
||||
const setScreenshotResponse = (resp: object | Error) => {
|
||||
const generateMock = Promise.resolve(() => ({
|
||||
pipe: () => ({
|
||||
toPromise: () => (resp instanceof Error ? Promise.reject(resp) : Promise.resolve(resp)),
|
||||
}),
|
||||
}));
|
||||
(generatePngObservableFactory as any).mockResolvedValue(generateMock);
|
||||
};
|
||||
|
||||
const config = {
|
||||
get: jest.fn(),
|
||||
kbnConfig: { get: jest.fn() },
|
||||
};
|
||||
const mockLogger = createMockLevelLogger();
|
||||
|
||||
beforeEach(async () => {
|
||||
({ server, httpSetup } = await setupServer(reportingSymbol));
|
||||
httpSetup.registerRouteHandlerContext(reportingSymbol, 'reporting', () => ({}));
|
||||
|
||||
const mockSetupDeps = ({
|
||||
elasticsearch: {
|
||||
legacy: { client: { callAsInternalUser: jest.fn() } },
|
||||
},
|
||||
router: httpSetup.createRouter(''),
|
||||
} as unknown) as any;
|
||||
|
||||
core = await createMockReportingCore(config, mockSetupDeps);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('returns a 200 by default', async () => {
|
||||
registerDiagnoseScreenshot(core, mockLogger);
|
||||
setScreenshotResponse({ warnings: [] });
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/screenshot')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"help": Array [],
|
||||
"logs": "",
|
||||
"success": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 200 when it fails and sets success to false', async () => {
|
||||
registerDiagnoseScreenshot(core, mockLogger);
|
||||
setScreenshotResponse({ warnings: [`Timeout waiting for .dank to load`] });
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/screenshot')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"help": Array [],
|
||||
"logs": Array [
|
||||
"Timeout waiting for .dank to load",
|
||||
],
|
||||
"success": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('catches errors and returns a well formed response', async () => {
|
||||
registerDiagnoseScreenshot(core, mockLogger);
|
||||
setScreenshotResponse(new Error('Failure to start chromium!'));
|
||||
await server.start();
|
||||
|
||||
await supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/screenshot')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
expect(body.help).toContain(`We couldn't screenshot your Kibana install.`);
|
||||
expect(body.logs).toContain(`Failure to start chromium!`);
|
||||
});
|
||||
});
|
||||
});
|
116
x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts
Normal file
116
x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ReportingCore } from '../..';
|
||||
import { API_DIAGNOSE_URL } from '../../../common/constants';
|
||||
import { omitBlacklistedHeaders } from '../../export_types/common';
|
||||
import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url';
|
||||
import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png';
|
||||
import { LevelLogger as Logger } from '../../lib';
|
||||
import { DiagnosticResponse } from '../../types';
|
||||
import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing';
|
||||
|
||||
export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Logger) => {
|
||||
const setupDeps = reporting.getPluginSetupDeps();
|
||||
const userHandler = authorizedUserPreRoutingFactory(reporting);
|
||||
const { router } = setupDeps;
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: `${API_DIAGNOSE_URL}/screenshot`,
|
||||
validate: {},
|
||||
},
|
||||
userHandler(async (user, context, req, res) => {
|
||||
const generatePngObservable = await generatePngObservableFactory(reporting);
|
||||
const config = reporting.getConfig();
|
||||
const decryptedHeaders = req.headers as Record<string, string>;
|
||||
const [basePath, protocol, hostname, port] = [
|
||||
config.kbnConfig.get('server', 'basePath'),
|
||||
config.get('kibanaServer', 'protocol'),
|
||||
config.get('kibanaServer', 'hostname'),
|
||||
config.get('kibanaServer', 'port'),
|
||||
] as string[];
|
||||
|
||||
const getAbsoluteUrl = getAbsoluteUrlFactory({
|
||||
defaultBasePath: basePath,
|
||||
protocol,
|
||||
hostname,
|
||||
port,
|
||||
});
|
||||
|
||||
const hashUrl = getAbsoluteUrl({
|
||||
basePath,
|
||||
path: '/',
|
||||
hash: '',
|
||||
search: '',
|
||||
});
|
||||
|
||||
// Hack the layout to make the base/login page work
|
||||
const layout = {
|
||||
id: 'png',
|
||||
dimensions: {
|
||||
width: 1440,
|
||||
height: 2024,
|
||||
},
|
||||
selectors: {
|
||||
screenshot: '.application',
|
||||
renderComplete: '.application',
|
||||
itemsCountAttribute: 'data-test-subj="kibanaChrome"',
|
||||
timefilterDurationAttribute: 'data-test-subj="kibanaChrome"',
|
||||
},
|
||||
};
|
||||
|
||||
const headers = {
|
||||
headers: omitBlacklistedHeaders({
|
||||
job: null,
|
||||
decryptedHeaders,
|
||||
}),
|
||||
conditions: {
|
||||
hostname,
|
||||
port: +port,
|
||||
basePath,
|
||||
protocol,
|
||||
},
|
||||
};
|
||||
|
||||
return generatePngObservable(logger, hashUrl, 'America/Los_Angeles', headers, layout)
|
||||
.pipe()
|
||||
.toPromise()
|
||||
.then((screenshot) => {
|
||||
if (screenshot.warnings.length) {
|
||||
return res.ok({
|
||||
body: {
|
||||
success: false,
|
||||
help: [],
|
||||
logs: screenshot.warnings,
|
||||
},
|
||||
});
|
||||
}
|
||||
return res.ok({
|
||||
body: {
|
||||
success: true,
|
||||
help: [],
|
||||
logs: '',
|
||||
} as DiagnosticResponse,
|
||||
});
|
||||
})
|
||||
.catch((error) =>
|
||||
res.ok({
|
||||
body: {
|
||||
success: false,
|
||||
help: [
|
||||
i18n.translate('xpack.reporting.diagnostic.screenshotFailureMessage', {
|
||||
defaultMessage: `We couldn't screenshot your Kibana install.`,
|
||||
}),
|
||||
],
|
||||
logs: error.message,
|
||||
} as DiagnosticResponse,
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
|
@ -11,8 +11,7 @@ import { setupServer } from 'src/core/server/test_utils';
|
|||
import supertest from 'supertest';
|
||||
import { ReportingCore } from '..';
|
||||
import { ExportTypesRegistry } from '../lib/export_types_registry';
|
||||
import { createMockReportingCore } from '../test_helpers';
|
||||
import { createMockLevelLogger } from '../test_helpers/create_mock_levellogger';
|
||||
import { createMockReportingCore, createMockLevelLogger } from '../test_helpers';
|
||||
import { registerJobGenerationRoutes } from './generation';
|
||||
|
||||
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
||||
|
|
|
@ -8,8 +8,10 @@ import { LevelLogger as Logger } from '../lib';
|
|||
import { registerJobGenerationRoutes } from './generation';
|
||||
import { registerJobInfoRoutes } from './jobs';
|
||||
import { ReportingCore } from '../core';
|
||||
import { registerDiagnosticRoutes } from './diagnostic';
|
||||
|
||||
export function registerRoutes(reporting: ReportingCore, logger: Logger) {
|
||||
registerJobGenerationRoutes(reporting, logger);
|
||||
registerJobInfoRoutes(reporting);
|
||||
registerDiagnosticRoutes(reporting, logger);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ jest.mock('../routes');
|
|||
jest.mock('../usage');
|
||||
jest.mock('../browsers');
|
||||
jest.mock('../lib/create_queue');
|
||||
jest.mock('../lib/validate');
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { ReportingConfig, ReportingCore } from '../';
|
||||
|
|
|
@ -8,3 +8,4 @@ export { createMockServer } from './create_mock_server';
|
|||
export { createMockReportingCore, createMockConfigSchema } from './create_mock_reportingplugin';
|
||||
export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory';
|
||||
export { createMockLayoutInstance } from './create_mock_layoutinstance';
|
||||
export { createMockLevelLogger } from './create_mock_levellogger';
|
||||
|
|
|
@ -162,3 +162,9 @@ export interface ExportTypeDefinition<
|
|||
runTaskFnFactory: RunTaskFnFactory<RunTaskFnType>;
|
||||
validLicenses: string[];
|
||||
}
|
||||
|
||||
export interface DiagnosticResponse {
|
||||
help: string[];
|
||||
success: boolean;
|
||||
logs: string;
|
||||
}
|
||||
|
|
|
@ -14089,8 +14089,6 @@
|
|||
"xpack.reporting.screencapture.waitingForRenderComplete": "レンダリングの完了を待っています",
|
||||
"xpack.reporting.screencapture.waitingForRenderedElements": "レンダリングされた {itemsCount} 個の要素が DOM に入るのを待っています",
|
||||
"xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "印刷用に最適化",
|
||||
"xpack.reporting.selfCheck.ok": "レポートプラグイン自己チェックOK!",
|
||||
"xpack.reporting.selfCheck.warning": "レポートプラグイン自己チェックで警告が発生しました: {err}",
|
||||
"xpack.reporting.serverConfig.autoSet.sandboxDisabled": "Chromiumサンドボックスは保護が強化されていますが、{osName} OSではサポートされていません。自動的に'{configKey}: true'を設定しています。",
|
||||
"xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromiumサンドボックスは保護が強化され、{osName} OSでサポートされています。自動的にChromiumサンドボックスを有効にしています。",
|
||||
"xpack.reporting.serverConfig.invalidServerHostname": "Kibana構成で「server.host:\"0\"」が見つかりました。これはReportingと互換性がありません。レポートが動作するように、「{configKey}:0.0.0.0」が自動的に構成になります。設定を「server.host:0.0.0.0」に変更するか、kibana.ymlに「{configKey}:0.0.0.0'」を追加して、このメッセージが表示されないようにすることができます。",
|
||||
|
|
|
@ -14098,8 +14098,6 @@
|
|||
"xpack.reporting.screencapture.waitingForRenderComplete": "正在等候渲染完成",
|
||||
"xpack.reporting.screencapture.waitingForRenderedElements": "正在等候 {itemsCount} 个已渲染元素进入 DOM",
|
||||
"xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "打印优化",
|
||||
"xpack.reporting.selfCheck.ok": "Reporting 插件自检正常!",
|
||||
"xpack.reporting.selfCheck.warning": "Reporting 插件自检生成警告:{err}",
|
||||
"xpack.reporting.serverConfig.autoSet.sandboxDisabled": "Chromium 沙盒提供附加保护层,但不受 {osName} OS 支持。自动设置“{configKey}: true”。",
|
||||
"xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromium 沙盒提供附加保护层,受 {osName} OS 支持。自动启用 Chromium 沙盒。",
|
||||
"xpack.reporting.serverConfig.invalidServerHostname": "在 Kibana 配置中找到“server.host:\"0\"”。其不与 Reporting 兼容。要使 Reporting 运行,“{configKey}:0.0.0.0”将自动添加到配置中。可以将该设置更改为“server.host:0.0.0.0”或在 kibana.yml 中添加“{configKey}:0.0.0.0”,以阻止此消息。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue