[Reporting] APM integration for baseline performance measurements (#59967)

* apm stuff

* fix cluster_client

* fix snapshot

* tracker utility for generate_pdf

* call apm.startSpan instead of txn.startSpan

* Fix async call to end transaction

* fix typescript

* remove captuureErrors

* restore accidental removal

* add startTrace lib

* fix import

* fix imports

* ts fix

* fix generate_png to not format base64 to buffer and back to base64

* 💅

* revert change to cluster client

* fix unused translation

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Tim Sullivan 2020-05-07 16:53:28 -07:00 committed by GitHub
parent d7f847e91d
commit 6bf0890186
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 243 additions and 61 deletions

View file

@ -6,16 +6,17 @@
import { i18n } from '@kbn/i18n';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers';
import { LevelLogger as Logger, startTrace } from '../../../../server/lib';
import { LayoutInstance } from '../../layouts/layout';
import { AttributesMap, ElementsPositionAndAttribute } from './types';
import { Logger } from '../../../../types';
import { CONTEXT_ELEMENTATTRIBUTES } from './constants';
import { AttributesMap, ElementsPositionAndAttribute } from './types';
export const getElementPositionAndAttributes = async (
browser: HeadlessBrowser,
layout: LayoutInstance,
logger: Logger
): Promise<ElementsPositionAndAttribute[] | null> => {
const endTrace = startTrace('get_element_position_data', 'read');
const { screenshot: screenshotSelector } = layout.selectors; // data-shared-items-container
let elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null;
try {
@ -69,5 +70,7 @@ export const getElementPositionAndAttributes = async (
elementsPositionAndAttributes = null;
}
endTrace();
return elementsPositionAndAttributes;
};

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers';
import { LevelLogger } from '../../../../server/lib';
import { LevelLogger, startTrace } from '../../../../server/lib';
import { CaptureConfig } from '../../../../server/types';
import { LayoutInstance } from '../../layouts/layout';
import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants';
@ -17,6 +17,7 @@ export const getNumberOfItems = async (
layout: LayoutInstance,
logger: LevelLogger
): Promise<number> => {
const endTrace = startTrace('get_number_of_items', 'read');
const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors;
let itemsCount: number;
@ -70,5 +71,7 @@ export const getNumberOfItems = async (
itemsCount = 1;
}
endTrace();
return itemsCount;
};

View file

@ -6,26 +6,9 @@
import { i18n } from '@kbn/i18n';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers';
import { LevelLogger } from '../../../../server/lib';
import { LevelLogger, startTrace } from '../../../../server/lib';
import { Screenshot, ElementsPositionAndAttribute } from './types';
const getAsyncDurationLogger = (logger: LevelLogger) => {
return async (description: string, promise: Promise<any>) => {
const start = Date.now();
const result = await promise;
logger.debug(
i18n.translate('xpack.reporting.screencapture.asyncTook', {
defaultMessage: '{description} took {took}ms',
values: {
description,
took: Date.now() - start,
},
})
);
return result;
};
};
export const getScreenshots = async (
browser: HeadlessBrowser,
elementsPositionAndAttributes: ElementsPositionAndAttribute[],
@ -37,21 +20,20 @@ export const getScreenshots = async (
})
);
const asyncDurationLogger = getAsyncDurationLogger(logger);
const screenshots: Screenshot[] = [];
for (let i = 0; i < elementsPositionAndAttributes.length; i++) {
const endTrace = startTrace('get_screenshots', 'read');
const item = elementsPositionAndAttributes[i];
const base64EncodedData = await asyncDurationLogger(
`screenshot #${i + 1}`,
browser.screenshot(item.position)
);
const base64EncodedData = await browser.screenshot(item.position);
screenshots.push({
base64EncodedData,
title: item.attributes.title,
description: item.attributes.description,
});
endTrace();
}
logger.info(

View file

@ -5,7 +5,7 @@
*/
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers';
import { LevelLogger } from '../../../../server/lib';
import { LevelLogger, startTrace } from '../../../../server/lib';
import { LayoutInstance } from '../../layouts/layout';
import { CONTEXT_GETTIMERANGE } from './constants';
import { TimeRange } from './types';
@ -15,6 +15,7 @@ export const getTimeRange = async (
layout: LayoutInstance,
logger: LevelLogger
): Promise<TimeRange | null> => {
const endTrace = startTrace('get_time_range', 'read');
logger.debug('getting timeRange');
const timeRange: TimeRange | null = await browser.evaluate(
@ -45,5 +46,7 @@ export const getTimeRange = async (
logger.debug('no timeRange');
}
endTrace();
return timeRange;
};

View file

@ -7,8 +7,8 @@
import { i18n } from '@kbn/i18n';
import fs from 'fs';
import { promisify } from 'util';
import { LevelLogger } from '../../../../server/lib';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers';
import { LevelLogger, startTrace } from '../../../../server/lib';
import { Layout } from '../../layouts/layout';
import { CONTEXT_INJECTCSS } from './constants';
@ -19,6 +19,7 @@ export const injectCustomCss = async (
layout: Layout,
logger: LevelLogger
): Promise<void> => {
const endTrace = startTrace('inject_css', 'correction');
logger.debug(
i18n.translate('xpack.reporting.screencapture.injectingCss', {
defaultMessage: 'injecting custom css',
@ -49,4 +50,6 @@ export const injectCustomCss = async (
})
);
}
endTrace();
};

View file

@ -4,8 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import apm from 'elastic-apm-node';
import * as Rx from 'rxjs';
import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators';
import {
catchError,
concatMap,
first,
mergeMap,
take,
takeUntil,
tap,
toArray,
} from 'rxjs/operators';
import { CaptureConfig } from '../../../../server/types';
import { DEFAULT_PAGELOAD_SELECTOR } from '../../constants';
import { HeadlessChromiumDriverFactory } from '../../../../types';
@ -41,6 +51,9 @@ export function screenshotsObservableFactory(
layout,
browserTimezone,
}: ScreenshotObservableOpts): Rx.Observable<ScreenshotResults[]> {
const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting');
const apmCreatePage = apmTrans?.startSpan('create_page', 'wait');
const create$ = browserDriverFactory.createPage(
{ viewport: layout.getBrowserViewport(), browserTimezone },
logger
@ -48,6 +61,7 @@ export function screenshotsObservableFactory(
return create$.pipe(
mergeMap(({ driver, exit$ }) => {
if (apmCreatePage) apmCreatePage.end();
return Rx.from(urls).pipe(
concatMap((url, index) => {
const setup$: Rx.Observable<ScreenSetupData> = Rx.of(1).pipe(
@ -81,10 +95,12 @@ export function screenshotsObservableFactory(
// allows for them to be displayed properly in many cases
await injectCustomCss(driver, layout, logger);
const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction');
if (layout.positionElements) {
// position panel elements for print layout
await layout.positionElements(driver, logger);
}
if (apmPositionElements) apmPositionElements.end();
await waitForRenderComplete(captureConfig, driver, layout, logger);
}),
@ -125,7 +141,10 @@ export function screenshotsObservableFactory(
toArray()
);
}),
first()
first(),
tap(() => {
if (apmTrans) apmTrans.end();
})
);
};
}

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers';
import { LevelLogger } from '../../../../server/lib';
import { LevelLogger, startTrace } from '../../../../server/lib';
import { CaptureConfig } from '../../../../server/types';
import { ConditionalHeaders } from '../../../../types';
@ -18,6 +18,7 @@ export const openUrl = async (
conditionalHeaders: ConditionalHeaders,
logger: LevelLogger
): Promise<void> => {
const endTrace = startTrace('open_url', 'wait');
try {
await browser.open(
url,
@ -32,11 +33,10 @@ export const openUrl = async (
throw new Error(
i18n.translate('xpack.reporting.screencapture.couldntLoadKibana', {
defaultMessage: `An error occurred when trying to open the Kibana URL. You may need to increase '{configKey}'. {error}`,
values: {
configKey: 'xpack.reporting.capture.timeouts.openUrl',
error: err,
},
values: { configKey: 'xpack.reporting.capture.timeouts.openUrl', error: err },
})
);
}
endTrace();
};

View file

@ -30,7 +30,7 @@ export interface ElementsPositionAndAttribute {
}
export interface Screenshot {
base64EncodedData: Buffer;
base64EncodedData: string;
title: string;
description: string;
}

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers';
import { LevelLogger } from '../../../../server/lib';
import { LevelLogger, startTrace } from '../../../../server/lib';
import { CaptureConfig } from '../../../../server/types';
import { LayoutInstance } from '../../layouts/layout';
import { CONTEXT_WAITFORRENDER } from './constants';
@ -17,6 +17,8 @@ export const waitForRenderComplete = async (
layout: LayoutInstance,
logger: LevelLogger
) => {
const endTrace = startTrace('wait_for_render', 'wait');
logger.debug(
i18n.translate('xpack.reporting.screencapture.waitingForRenderComplete', {
defaultMessage: 'waiting for rendering to complete',
@ -76,5 +78,7 @@ export const waitForRenderComplete = async (
defaultMessage: 'rendering is complete',
})
);
endTrace();
});
};

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers';
import { LevelLogger } from '../../../../server/lib';
import { LevelLogger, startTrace } from '../../../../server/lib';
import { CaptureConfig } from '../../../../server/types';
import { LayoutInstance } from '../../layouts/layout';
import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants';
@ -29,6 +29,7 @@ export const waitForVisualizations = async (
layout: LayoutInstance,
logger: LevelLogger
): Promise<void> => {
const endTrace = startTrace('wait_for_visualizations', 'wait');
const { renderComplete: renderCompleteSelector } = layout.selectors;
logger.debug(
@ -63,4 +64,6 @@ export const waitForVisualizations = async (
})
);
}
endTrace();
};

View file

@ -126,7 +126,7 @@ test(`returns content_type of application/png`, async () => {
const encryptedHeaders = await encryptHeaders({});
const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock;
generatePngObservable.mockReturnValue(Rx.of(Buffer.from('')));
generatePngObservable.mockReturnValue(Rx.of('foo'));
const { content_type: contentType } = await executeJob(
'pngJobId',
@ -137,10 +137,10 @@ test(`returns content_type of application/png`, async () => {
});
test(`returns content of generatePng getBuffer base64 encoded`, async () => {
const testContent = 'test content';
const testContent = 'raw string from get_screenhots';
const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock;
generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) }));
generatePngObservable.mockReturnValue(Rx.of({ base64: testContent }));
const executeJob = await executeJobFactory(mockReporting, getMockLogger());
const encryptedHeaders = await encryptHeaders({});
@ -150,5 +150,5 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => {
cancellationToken
);
expect(content).toEqual(Buffer.from(testContent).toString('base64'));
expect(content).toEqual(testContent);
});

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import apm from 'elastic-apm-node';
import * as Rx from 'rxjs';
import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators';
import { PNG_JOB_TYPE } from '../../../../common/constants';
@ -29,6 +30,10 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut
const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']);
return async function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) {
const apmTrans = apm.startTransaction('reporting execute_job png', 'reporting');
const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup');
let apmGeneratePng: { end: () => void } | null | undefined;
const generatePngObservable = await generatePngObservableFactory(reporting);
const jobLogger = logger.clone([jobId]);
const process$: Rx.Observable<JobDocOutput> = Rx.of(1).pipe(
@ -38,6 +43,9 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut
mergeMap(conditionalHeaders => {
const urls = getFullUrls({ config, job });
const hashUrl = urls[0];
if (apmGetAssets) apmGetAssets.end();
apmGeneratePng = apmTrans?.startSpan('generate_png_pipeline', 'execute');
return generatePngObservable(
jobLogger,
hashUrl,
@ -46,11 +54,14 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut
job.layout
);
}),
map(({ buffer, warnings }) => {
map(({ base64, warnings }) => {
if (apmGeneratePng) apmGeneratePng.end();
return {
content_type: 'image/png',
content: buffer.toString('base64'),
size: buffer.byteLength,
content: base64,
size: (base64 && base64.length) || 0,
warnings,
};
}),

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import apm from 'elastic-apm-node';
import * as Rx from 'rxjs';
import { map } from 'rxjs/operators';
import { ReportingCore } from '../../../../server';
@ -22,12 +23,16 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
browserTimezone: string,
conditionalHeaders: ConditionalHeaders,
layoutParams: LayoutParams
): Rx.Observable<{ buffer: Buffer; warnings: string[] }> {
): Rx.Observable<{ base64: string | null; warnings: string[] }> {
const apmTrans = apm.startTransaction('reporting generate_png', 'reporting');
const apmLayout = apmTrans?.startSpan('create_layout', 'setup');
if (!layoutParams || !layoutParams.dimensions) {
throw new Error(`LayoutParams.Dimensions is undefined.`);
}
const layout = new PreserveLayout(layoutParams.dimensions);
if (apmLayout) apmLayout.end();
const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup');
const screenshots$ = getScreenshots({
logger,
urls: [url],
@ -36,8 +41,11 @@ export async function generatePngObservableFactory(reporting: ReportingCore) {
browserTimezone,
}).pipe(
map((results: ScreenshotResults[]) => {
if (apmScreenshots) apmScreenshots.end();
if (apmTrans) apmTrans.end();
return {
buffer: results[0].screenshots[0].base64EncodedData,
base64: results[0].screenshots[0].base64EncodedData,
warnings: results.reduce((found, current) => {
if (current.error) {
found.push(current.error.message);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import apm from 'elastic-apm-node';
import * as Rx from 'rxjs';
import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators';
import { PDF_JOB_TYPE } from '../../../../common/constants';
@ -31,6 +32,10 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut
const logger = parentLogger.clone([PDF_JOB_TYPE, 'execute']);
return async function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) {
const apmTrans = apm.startTransaction('reporting execute_job pdf', 'reporting');
const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup');
let apmGeneratePdf: { end: () => void } | null | undefined;
const generatePdfObservable = await generatePdfObservableFactory(reporting);
const jobLogger = logger.clone([jobId]);
@ -43,6 +48,9 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut
const urls = getFullUrls({ config, job });
const { browserTimezone, layout, title } = job;
if (apmGetAssets) apmGetAssets.end();
apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute');
return generatePdfObservable(
jobLogger,
title,
@ -53,12 +61,20 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut
logo
);
}),
map(({ buffer, warnings }) => ({
content_type: 'application/pdf',
content: buffer.toString('base64'),
size: buffer.byteLength,
warnings,
})),
map(({ buffer, warnings }) => {
if (apmGeneratePdf) apmGeneratePdf.end();
const apmEncode = apmTrans?.startSpan('encode_pdf', 'output');
const content = buffer?.toString('base64') || null;
if (apmEncode) apmEncode.end();
return {
content_type: 'application/pdf',
content,
size: buffer?.byteLength || 0,
warnings,
};
}),
catchError(err => {
jobLogger.error(err);
return Rx.throwError(err);
@ -66,6 +82,8 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut
);
const stop$ = Rx.fromEventPattern(cancellationToken.on);
if (apmTrans) apmTrans.end();
return process$.pipe(takeUntil(stop$)).toPromise();
};
};

View file

@ -13,6 +13,7 @@ import { ConditionalHeaders } from '../../../../types';
import { createLayout } from '../../../common/layouts';
import { LayoutInstance, LayoutParams } from '../../../common/layouts/layout';
import { ScreenshotResults } from '../../../common/lib/screenshots/types';
import { getTracker } from './tracker';
// @ts-ignore untyped module
import { pdf } from './pdf';
@ -39,8 +40,14 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
conditionalHeaders: ConditionalHeaders,
layoutParams: LayoutParams,
logo?: string
): Rx.Observable<{ buffer: Buffer; warnings: string[] }> {
): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> {
const tracker = getTracker();
tracker.startLayout();
const layout = createLayout(captureConfig, layoutParams) as LayoutInstance;
tracker.endLayout();
tracker.startScreenshots();
const screenshots$ = getScreenshots({
logger,
urls,
@ -49,16 +56,22 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
browserTimezone,
}).pipe(
mergeMap(async (results: ScreenshotResults[]) => {
const pdfOutput = pdf.create(layout, logo);
tracker.endScreenshots();
tracker.startSetup();
const pdfOutput = pdf.create(layout, logo);
if (title) {
const timeRange = getTimeRange(results);
title += timeRange ? ` - ${timeRange.duration}` : '';
pdfOutput.setTitle(title);
}
tracker.endSetup();
results.forEach(r => {
r.screenshots.forEach(screenshot => {
logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.base64EncodedData?.length || 0}`); // prettier-ignore
tracker.startAddImage();
tracker.endAddImage();
pdfOutput.addImage(screenshot.base64EncodedData, {
title: screenshot.title,
description: screenshot.description,
@ -66,10 +79,26 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) {
});
});
pdfOutput.generate();
let buffer: Buffer | null = null;
try {
tracker.startCompile();
logger.debug(`Compiling PDF...`);
pdfOutput.generate();
tracker.endCompile();
tracker.startGetBuffer();
logger.debug(`Generating PDF Buffer...`);
buffer = await pdfOutput.getBuffer();
logger.debug(`PDF buffer byte length: ${buffer?.byteLength || 0}`);
tracker.endGetBuffer();
} catch (err) {
logger.error(`Could not generate the PDF buffer! ${err}`);
}
tracker.end();
return {
buffer: await pdfOutput.getBuffer(),
buffer,
warnings: results.reduce((found, current) => {
if (current.error) {
found.push(current.error.message);

View file

@ -0,0 +1,83 @@
/*
* 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 apm from 'elastic-apm-node';
interface PdfTracker {
startLayout: () => void;
endLayout: () => void;
startScreenshots: () => void;
endScreenshots: () => void;
startSetup: () => void;
endSetup: () => void;
startAddImage: () => void;
endAddImage: () => void;
startCompile: () => void;
endCompile: () => void;
startGetBuffer: () => void;
endGetBuffer: () => void;
end: () => void;
}
const SPANTYPE_SETUP = 'setup';
const SPANTYPE_OUTPUT = 'output';
interface ApmSpan {
end: () => void;
}
export function getTracker(): PdfTracker {
const apmTrans = apm.startTransaction('reporting generate_pdf', 'reporting');
let apmLayout: ApmSpan | null = null;
let apmScreenshots: ApmSpan | null = null;
let apmSetup: ApmSpan | null = null;
let apmAddImage: ApmSpan | null = null;
let apmCompilePdf: ApmSpan | null = null;
let apmGetBuffer: ApmSpan | null = null;
return {
startLayout() {
apmLayout = apmTrans?.startSpan('create_layout', SPANTYPE_SETUP) || null;
},
endLayout() {
if (apmLayout) apmLayout.end();
},
startScreenshots() {
apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', SPANTYPE_SETUP) || null;
},
endScreenshots() {
if (apmScreenshots) apmScreenshots.end();
},
startSetup() {
apmSetup = apmTrans?.startSpan('setup_pdf', SPANTYPE_SETUP) || null;
},
endSetup() {
if (apmSetup) apmSetup.end();
},
startAddImage() {
apmAddImage = apmTrans?.startSpan('add_pdf_image', SPANTYPE_OUTPUT) || null;
},
endAddImage() {
if (apmAddImage) apmAddImage.end();
},
startCompile() {
apmCompilePdf = apmTrans?.startSpan('compile_pdf', SPANTYPE_OUTPUT) || null;
},
endCompile() {
if (apmCompilePdf) apmCompilePdf.end();
},
startGetBuffer() {
apmGetBuffer = apmTrans?.startSpan('get_buffer', SPANTYPE_OUTPUT) || null;
},
endGetBuffer() {
if (apmGetBuffer) apmGetBuffer.end();
},
end() {
if (apmTrans) apmTrans.end();
},
};
}

View file

@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { LevelLogger } from './level_logger';
export { checkLicenseFactory } from './check_license';
export { createQueueFactory } from './create_queue';
export { cryptoFactory } from './crypto';
export { enqueueJobFactory } from './enqueue_job';
export { getExportTypesRegistry } from './export_types_registry';
export { LevelLogger } from './level_logger';
export { runValidations } from './validate';
export { startTrace } from './trace';

View file

@ -0,0 +1,14 @@
/*
* 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 apm from 'elastic-apm-node';
export function startTrace(name: string, category: string) {
const span = apm.startSpan(name, category);
return () => {
if (span) span.end();
};
}

View file

@ -12200,7 +12200,6 @@
"xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "{reportObjectType}「{reportObjectTitle}」のレポートが作成されました",
"xpack.reporting.registerFeature.reportingDescription": "ディスカバリ、可視化、ダッシュボードから生成されたレポートを管理します。",
"xpack.reporting.registerFeature.reportingTitle": "レポート",
"xpack.reporting.screencapture.asyncTook": "{description} にかかった時間は {took}ms でした",
"xpack.reporting.screencapture.couldntFinishRendering": "{count} 件のビジュアライゼーションのレンダリングが完了するのを待つ間にエラーが発生しました。「{configKey}」を増やす必要があるかもしれません。 {error}",
"xpack.reporting.screencapture.couldntLoadKibana": "Kibana URL を開こうとするときにエラーが発生しました。「{configKey}」を増やす必要があるかもしれません。 {error}",
"xpack.reporting.screencapture.injectCss": "Kibana CSS をレポート用に更新しようとしたときにエラーが発生しました。{error}",

View file

@ -12207,7 +12207,6 @@
"xpack.reporting.publicNotifier.successfullyCreatedReportNotificationTitle": "已为 {reportObjectType}“{reportObjectTitle}”创建报告",
"xpack.reporting.registerFeature.reportingDescription": "管理您从 Discover、Visualize 和 Dashboard 生成的报告。",
"xpack.reporting.registerFeature.reportingTitle": "报告",
"xpack.reporting.screencapture.asyncTook": "{description} 花费了 {took}ms",
"xpack.reporting.screencapture.couldntFinishRendering": "尝试等候 {count} 个可视化完成渲染时发生错误。您可能需要增加“{configKey}”。{error}",
"xpack.reporting.screencapture.couldntLoadKibana": "尝试打开 Kibana URL 时发生了错误。您可能需要增加“{configKey}”。{error}",
"xpack.reporting.screencapture.injectCss": "尝试为 Reporting 更新 Kibana CSS 时发生错误。{error}",