mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 10:23:14 -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.
|
Having trouble? Here are solutions to common problems you might encounter while using Reporting.
|
||||||
|
|
||||||
|
* <<reporting-diagnostics>>
|
||||||
* <<reporting-troubleshooting-system-dependencies>>
|
* <<reporting-troubleshooting-system-dependencies>>
|
||||||
* <<reporting-troubleshooting-text-incorrect>>
|
* <<reporting-troubleshooting-text-incorrect>>
|
||||||
* <<reporting-troubleshooting-missing-data>>
|
* <<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-puppeteer-debug-logs>>
|
||||||
* <<reporting-troubleshooting-system-requirements>>
|
* <<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]
|
[float]
|
||||||
[[reporting-troubleshooting-system-dependencies]]
|
[[reporting-troubleshooting-system-dependencies]]
|
||||||
=== 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_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`;
|
||||||
export const API_LIST_URL = '/api/reporting/jobs';
|
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_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 CONTENT_TYPE_CSV = 'text/csv';
|
||||||
export const CSV_REPORTING_ACTION = 'downloadCsvReport';
|
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 {
|
import {
|
||||||
EuiBasicTable,
|
EuiBasicTable,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiFlexGroup,
|
||||||
EuiPageContent,
|
EuiPageContent,
|
||||||
EuiSpacer,
|
EuiSpacer,
|
||||||
EuiText,
|
EuiText,
|
||||||
|
@ -31,6 +33,7 @@ import {
|
||||||
ReportErrorButton,
|
ReportErrorButton,
|
||||||
ReportInfoButton,
|
ReportInfoButton,
|
||||||
} from './buttons';
|
} from './buttons';
|
||||||
|
import { ReportDiagnostic } from './report_diagnostic';
|
||||||
|
|
||||||
export interface Job {
|
export interface Job {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -134,10 +137,16 @@ class ReportListingUi extends Component<Props, State> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
<EuiPageContent horizontalPosition="center" className="euiPageBody--restrictWidth-default">
|
<EuiPageContent horizontalPosition="center" className="euiPageBody--restrictWidth-default">
|
||||||
|
<EuiFlexGroup justifyContent="spaceBetween">
|
||||||
|
<EuiFlexItem grow={false}>
|
||||||
<EuiTitle>
|
<EuiTitle>
|
||||||
<h1>
|
<h1>
|
||||||
<FormattedMessage id="xpack.reporting.listing.reportstitle" defaultMessage="Reports" />
|
<FormattedMessage
|
||||||
|
id="xpack.reporting.listing.reportstitle"
|
||||||
|
defaultMessage="Reports"
|
||||||
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
<EuiText color="subdued" size="s">
|
<EuiText color="subdued" size="s">
|
||||||
|
@ -148,9 +157,18 @@ class ReportListingUi extends Component<Props, State> {
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</EuiText>
|
</EuiText>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
<EuiSpacer />
|
<EuiSpacer />
|
||||||
{this.renderTable()}
|
{this.renderTable()}
|
||||||
</EuiPageContent>
|
</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 rison from 'rison-node';
|
||||||
import { HttpSetup } from 'src/core/public';
|
import { HttpSetup } from 'src/core/public';
|
||||||
import { JobId, SourceJob } from '../../common/types';
|
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';
|
import { add } from './job_completion_notifications';
|
||||||
|
|
||||||
export interface JobQueueEntry {
|
export interface JobQueueEntry {
|
||||||
|
@ -59,6 +64,12 @@ interface JobParams {
|
||||||
[paramName: string]: any;
|
[paramName: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DiagnoseResponse {
|
||||||
|
help: string[];
|
||||||
|
success: boolean;
|
||||||
|
logs: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class ReportingAPIClient {
|
export class ReportingAPIClient {
|
||||||
private http: HttpSetup;
|
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
|
* provides the raw server basePath to allow it to be stripped out from relativeUrls in job params
|
||||||
*/
|
*/
|
||||||
public getServerBasePath = () => this.http.basePath.serverBasePath;
|
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;
|
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
|
* 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.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import del from 'del';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import del from 'del';
|
|
||||||
|
|
||||||
import * as Rx from 'rxjs';
|
import * as Rx from 'rxjs';
|
||||||
import { LevelLogger } from '../lib';
|
import { LevelLogger } from '../lib';
|
||||||
|
import { paths } from './chromium/paths';
|
||||||
import { ensureBrowserDownloaded } from './download';
|
import { ensureBrowserDownloaded } from './download';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { md5 } from './download/checksum';
|
import { md5 } from './download/checksum';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { extract } from './extract';
|
import { extract } from './extract';
|
||||||
import { paths } from './chromium/paths';
|
|
||||||
|
|
||||||
interface Package {
|
interface Package {
|
||||||
platforms: string[];
|
platforms: string[];
|
||||||
architecture: 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
|
* "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
|
* 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}`);
|
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(() => '');
|
const binaryChecksum = await md5(binaryPath).catch(() => '');
|
||||||
|
|
||||||
if (binaryChecksum !== pkg.binaryChecksum) {
|
if (binaryChecksum !== pkg.binaryChecksum) {
|
||||||
|
|
|
@ -28,7 +28,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
|
||||||
if (!layoutParams || !layoutParams.dimensions) {
|
if (!layoutParams || !layoutParams.dimensions) {
|
||||||
throw new Error(`LayoutParams.Dimensions is undefined.`);
|
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();
|
if (apmLayout) apmLayout.end();
|
||||||
|
|
||||||
const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup');
|
const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup');
|
||||||
|
|
|
@ -13,4 +13,3 @@ export { LevelLogger } from './level_logger';
|
||||||
export { statuses } from './statuses';
|
export { statuses } from './statuses';
|
||||||
export { ReportingStore } from './store';
|
export { ReportingStore } from './store';
|
||||||
export { startTrace } from './trace';
|
export { startTrace } from './trace';
|
||||||
export { runValidations } from './validate';
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ export interface Size {
|
||||||
export interface LayoutParams {
|
export interface LayoutParams {
|
||||||
id: string;
|
id: string;
|
||||||
dimensions: Size;
|
dimensions: Size;
|
||||||
|
selectors?: LayoutSelectorDictionary;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LayoutSelectors {
|
interface LayoutSelectors {
|
||||||
|
|
|
@ -25,12 +25,16 @@ export class PreserveLayout extends Layout {
|
||||||
private readonly scaledHeight: number;
|
private readonly scaledHeight: number;
|
||||||
private readonly scaledWidth: number;
|
private readonly scaledWidth: number;
|
||||||
|
|
||||||
constructor(size: Size) {
|
constructor(size: Size, layoutSelectors?: LayoutSelectorDictionary) {
|
||||||
super(LayoutTypes.PRESERVE_LAYOUT);
|
super(LayoutTypes.PRESERVE_LAYOUT);
|
||||||
this.height = size.height;
|
this.height = size.height;
|
||||||
this.width = size.width;
|
this.width = size.width;
|
||||||
this.scaledHeight = size.height * ZOOM;
|
this.scaledHeight = size.height * ZOOM;
|
||||||
this.scaledWidth = size.width * ZOOM;
|
this.scaledWidth = size.width * ZOOM;
|
||||||
|
|
||||||
|
if (layoutSelectors) {
|
||||||
|
this.selectors = layoutSelectors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCssOverridesPath() {
|
public getCssOverridesPath() {
|
||||||
|
|
|
@ -7,8 +7,7 @@
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { ElasticsearchServiceSetup } from 'src/core/server';
|
import { ElasticsearchServiceSetup } from 'src/core/server';
|
||||||
import { ReportingConfig, ReportingCore } from '../..';
|
import { ReportingConfig, ReportingCore } from '../..';
|
||||||
import { createMockReportingCore } from '../../test_helpers';
|
import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers';
|
||||||
import { createMockLevelLogger } from '../../test_helpers/create_mock_levellogger';
|
|
||||||
import { Report } from './report';
|
import { Report } from './report';
|
||||||
import { ReportingStore } from './store';
|
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 { ReportingCore } from './';
|
||||||
import { initializeBrowserDriverFactory } from './browsers';
|
import { initializeBrowserDriverFactory } from './browsers';
|
||||||
import { buildConfig, ReportingConfigType } from './config';
|
import { buildConfig, ReportingConfigType } from './config';
|
||||||
import { createQueueFactory, LevelLogger, ReportingStore, runValidations } from './lib';
|
import { createQueueFactory, LevelLogger, ReportingStore } from './lib';
|
||||||
import { registerRoutes } from './routes';
|
import { registerRoutes } from './routes';
|
||||||
import { setFieldFormats } from './services';
|
import { setFieldFormats } from './services';
|
||||||
import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types';
|
import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types';
|
||||||
|
@ -105,7 +105,6 @@ export class ReportingPlugin
|
||||||
setFieldFormats(plugins.data.fieldFormats);
|
setFieldFormats(plugins.data.fieldFormats);
|
||||||
|
|
||||||
const { logger, reportingCore } = this;
|
const { logger, reportingCore } = this;
|
||||||
const { elasticsearch } = reportingCore.getPluginSetupDeps();
|
|
||||||
|
|
||||||
// async background start
|
// async background start
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -124,9 +123,6 @@ export class ReportingPlugin
|
||||||
store,
|
store,
|
||||||
});
|
});
|
||||||
|
|
||||||
// run self-check validations
|
|
||||||
runValidations(config, elasticsearch, browserDriverFactory, this.logger);
|
|
||||||
|
|
||||||
this.logger.debug('Start complete');
|
this.logger.debug('Start complete');
|
||||||
})().catch((e) => {
|
})().catch((e) => {
|
||||||
this.logger.error(`Error in Reporting start, reporting may not function properly`);
|
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 supertest from 'supertest';
|
||||||
import { ReportingCore } from '..';
|
import { ReportingCore } from '..';
|
||||||
import { ExportTypesRegistry } from '../lib/export_types_registry';
|
import { ExportTypesRegistry } from '../lib/export_types_registry';
|
||||||
import { createMockReportingCore } from '../test_helpers';
|
import { createMockReportingCore, createMockLevelLogger } from '../test_helpers';
|
||||||
import { createMockLevelLogger } from '../test_helpers/create_mock_levellogger';
|
|
||||||
import { registerJobGenerationRoutes } from './generation';
|
import { registerJobGenerationRoutes } from './generation';
|
||||||
|
|
||||||
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
||||||
|
|
|
@ -8,8 +8,10 @@ import { LevelLogger as Logger } from '../lib';
|
||||||
import { registerJobGenerationRoutes } from './generation';
|
import { registerJobGenerationRoutes } from './generation';
|
||||||
import { registerJobInfoRoutes } from './jobs';
|
import { registerJobInfoRoutes } from './jobs';
|
||||||
import { ReportingCore } from '../core';
|
import { ReportingCore } from '../core';
|
||||||
|
import { registerDiagnosticRoutes } from './diagnostic';
|
||||||
|
|
||||||
export function registerRoutes(reporting: ReportingCore, logger: Logger) {
|
export function registerRoutes(reporting: ReportingCore, logger: Logger) {
|
||||||
registerJobGenerationRoutes(reporting, logger);
|
registerJobGenerationRoutes(reporting, logger);
|
||||||
registerJobInfoRoutes(reporting);
|
registerJobInfoRoutes(reporting);
|
||||||
|
registerDiagnosticRoutes(reporting, logger);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ jest.mock('../routes');
|
||||||
jest.mock('../usage');
|
jest.mock('../usage');
|
||||||
jest.mock('../browsers');
|
jest.mock('../browsers');
|
||||||
jest.mock('../lib/create_queue');
|
jest.mock('../lib/create_queue');
|
||||||
jest.mock('../lib/validate');
|
|
||||||
|
|
||||||
import * as Rx from 'rxjs';
|
import * as Rx from 'rxjs';
|
||||||
import { ReportingConfig, ReportingCore } from '../';
|
import { ReportingConfig, ReportingCore } from '../';
|
||||||
|
|
|
@ -8,3 +8,4 @@ export { createMockServer } from './create_mock_server';
|
||||||
export { createMockReportingCore, createMockConfigSchema } from './create_mock_reportingplugin';
|
export { createMockReportingCore, createMockConfigSchema } from './create_mock_reportingplugin';
|
||||||
export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory';
|
export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory';
|
||||||
export { createMockLayoutInstance } from './create_mock_layoutinstance';
|
export { createMockLayoutInstance } from './create_mock_layoutinstance';
|
||||||
|
export { createMockLevelLogger } from './create_mock_levellogger';
|
||||||
|
|
|
@ -162,3 +162,9 @@ export interface ExportTypeDefinition<
|
||||||
runTaskFnFactory: RunTaskFnFactory<RunTaskFnType>;
|
runTaskFnFactory: RunTaskFnFactory<RunTaskFnType>;
|
||||||
validLicenses: string[];
|
validLicenses: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DiagnosticResponse {
|
||||||
|
help: string[];
|
||||||
|
success: boolean;
|
||||||
|
logs: string;
|
||||||
|
}
|
||||||
|
|
|
@ -14089,8 +14089,6 @@
|
||||||
"xpack.reporting.screencapture.waitingForRenderComplete": "レンダリングの完了を待っています",
|
"xpack.reporting.screencapture.waitingForRenderComplete": "レンダリングの完了を待っています",
|
||||||
"xpack.reporting.screencapture.waitingForRenderedElements": "レンダリングされた {itemsCount} 個の要素が DOM に入るのを待っています",
|
"xpack.reporting.screencapture.waitingForRenderedElements": "レンダリングされた {itemsCount} 個の要素が DOM に入るのを待っています",
|
||||||
"xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "印刷用に最適化",
|
"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.sandboxDisabled": "Chromiumサンドボックスは保護が強化されていますが、{osName} OSではサポートされていません。自動的に'{configKey}: true'を設定しています。",
|
||||||
"xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromiumサンドボックスは保護が強化され、{osName} OSでサポートされています。自動的にChromiumサンドボックスを有効にしています。",
|
"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'」を追加して、このメッセージが表示されないようにすることができます。",
|
"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.waitingForRenderComplete": "正在等候渲染完成",
|
||||||
"xpack.reporting.screencapture.waitingForRenderedElements": "正在等候 {itemsCount} 个已渲染元素进入 DOM",
|
"xpack.reporting.screencapture.waitingForRenderedElements": "正在等候 {itemsCount} 个已渲染元素进入 DOM",
|
||||||
"xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "打印优化",
|
"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.sandboxDisabled": "Chromium 沙盒提供附加保护层,但不受 {osName} OS 支持。自动设置“{configKey}: true”。",
|
||||||
"xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromium 沙盒提供附加保护层,受 {osName} OS 支持。自动启用 Chromium 沙盒。",
|
"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”,以阻止此消息。",
|
"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