diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts index 2f93765165e50d1..29d3dbce4a2cade 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts @@ -5,17 +5,20 @@ */ import { i18n } from '@kbn/i18n'; +import apm from 'elastic-apm-node'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; -import { LayoutInstance } from '../../layouts/layout'; -import { AttributesMap, ElementsPositionAndAttribute } from './types'; import { Logger } from '../../../../types'; +import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; +import { ApmTransaction, AttributesMap, ElementsPositionAndAttribute } from './types'; export const getElementPositionAndAttributes = async ( browser: HeadlessBrowser, layout: LayoutInstance, - logger: Logger + logger: Logger, + txn: ApmTransaction ): Promise => { + const apmSpan = txn?.startSpan('get_element_position_data', 'read'); const { screenshot: screenshotSelector } = layout.selectors; // data-shared-items-container let elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; try { @@ -66,8 +69,10 @@ export const getElementPositionAndAttributes = async ( ); } } catch (err) { + apm.captureError(err); elementsPositionAndAttributes = null; } + if (apmSpan) apmSpan.end(); return elementsPositionAndAttributes; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts index 16eb433e8a75e30..e88956f30328587 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_number_of_items.ts @@ -4,19 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { ServerFacade } from '../../../../types'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; +import { ApmTransaction } from './types'; export const getNumberOfItems = async ( server: ServerFacade, browser: HeadlessBrowser, layout: LayoutInstance, - logger: LevelLogger + logger: LevelLogger, + txn: ApmTransaction | null ): Promise => { + const apmSpan = txn?.startSpan('get_number_of_items', 'read'); const config = server.config(); const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors; let itemsCount: number; @@ -59,6 +63,7 @@ export const getNumberOfItems = async ( logger ); } catch (err) { + apm.captureError(err); throw new Error( i18n.translate('xpack.reporting.screencapture.readVisualizationsError', { defaultMessage: `An error occurred when trying to read the page for visualization panel info. You may need to increase '{configKey}'. {error}`, @@ -71,5 +76,6 @@ export const getNumberOfItems = async ( itemsCount = 1; } + if (apmSpan) apmSpan.end(); return itemsCount; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts index d50ac64743f0784..593170c5f036600 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_screenshots.ts @@ -7,29 +7,13 @@ import { i18n } from '@kbn/i18n'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; -import { Screenshot, ElementsPositionAndAttribute } from './types'; - -const getAsyncDurationLogger = (logger: LevelLogger) => { - return async (description: string, promise: Promise) => { - 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; - }; -}; +import { ApmTransaction, Screenshot, ElementsPositionAndAttribute } from './types'; export const getScreenshots = async ( browser: HeadlessBrowser, elementsPositionAndAttributes: ElementsPositionAndAttribute[], - logger: LevelLogger + logger: LevelLogger, + txn: ApmTransaction ): Promise => { logger.info( i18n.translate('xpack.reporting.screencapture.takingScreenshots', { @@ -37,21 +21,20 @@ export const getScreenshots = async ( }) ); - const asyncDurationLogger = getAsyncDurationLogger(logger); const screenshots: Screenshot[] = []; for (let i = 0; i < elementsPositionAndAttributes.length; i++) { + const apmSpan = txn?.startSpan('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)).toString('base64'); screenshots.push({ base64EncodedData, title: item.attributes.title, description: item.attributes.description, }); + + if (apmSpan) apmSpan.end(); } logger.info( diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts index c1c43ed45259412..4125bf1046a53eb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_time_range.ts @@ -9,12 +9,15 @@ import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_GETTIMERANGE } from './constants'; import { TimeRange } from './types'; +import { ApmTransaction } from './types'; export const getTimeRange = async ( browser: HeadlessBrowser, layout: LayoutInstance, - logger: LevelLogger + logger: LevelLogger, + txn: ApmTransaction ): Promise => { + const apmSpan = txn?.startSpan('get_time_range', 'read'); logger.debug('getting timeRange'); const timeRange: TimeRange | null = await browser.evaluate( @@ -45,5 +48,6 @@ export const getTimeRange = async ( logger.debug('no timeRange'); } + if (apmSpan) apmSpan.end(); return timeRange; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts index cb2673e85186bab..688129a377f4678 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/inject_css.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import { i18n } from '@kbn/i18n'; import fs from 'fs'; import { promisify } from 'util'; @@ -11,14 +12,17 @@ import { LevelLogger } from '../../../../server/lib'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { Layout } from '../../layouts/layout'; import { CONTEXT_INJECTCSS } from './constants'; +import { ApmTransaction } from './types'; const fsp = { readFile: promisify(fs.readFile) }; export const injectCustomCss = async ( browser: HeadlessBrowser, layout: Layout, - logger: LevelLogger + logger: LevelLogger, + txn: ApmTransaction ): Promise => { + const apmSpan = txn?.startSpan('inject_css', 'correction'); logger.debug( i18n.translate('xpack.reporting.screencapture.injectingCss', { defaultMessage: 'injecting custom css', @@ -42,6 +46,7 @@ export const injectCustomCss = async ( logger ); } catch (err) { + apm.captureError(err); throw new Error( i18n.translate('xpack.reporting.screencapture.injectCss', { defaultMessage: `An error occurred when trying to update Kibana CSS for reporting. {error}`, @@ -49,4 +54,6 @@ export const injectCustomCss = async ( }) ); } + + if (apmSpan) apmSpan.end(); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts index 878a9d3b8739324..707f0090369edad 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -4,19 +4,29 @@ * 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, HeadlessChromiumDriverFactory, ServerFacade } from '../../../../types'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getNumberOfItems } from './get_number_of_items'; import { getScreenshots } from './get_screenshots'; import { getTimeRange } from './get_time_range'; +import { injectCustomCss } from './inject_css'; import { openUrl } from './open_url'; import { skipTelemetry } from './skip_telemetry'; import { ScreenSetupData, ScreenshotObservableOpts, ScreenshotResults } from './types'; import { waitForRenderComplete } from './wait_for_render'; import { waitForVisualizations } from './wait_for_visualizations'; -import { injectCustomCss } from './inject_css'; export function screenshotsObservableFactory( server: ServerFacade, @@ -32,48 +42,57 @@ export function screenshotsObservableFactory( layout, browserTimezone, }: ScreenshotObservableOpts): Rx.Observable { + const txn = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); + + const apmCreatePage = txn?.startSpan('create_page', 'wait'); const create$ = browserDriverFactory.createPage( { viewport: layout.getBrowserViewport(), browserTimezone }, logger ); + return Rx.from(urls).pipe( concatMap(url => { return create$.pipe( mergeMap(({ driver, exit$ }) => { + if (apmCreatePage) apmCreatePage.end(); + const setup$: Rx.Observable = Rx.of(1).pipe( takeUntil(exit$), - mergeMap(() => openUrl(server, driver, url, conditionalHeaders, logger)), - mergeMap(() => skipTelemetry(driver, logger)), - mergeMap(() => getNumberOfItems(server, driver, layout, logger)), + mergeMap(() => openUrl(server, driver, url, conditionalHeaders, logger, txn)), + mergeMap(() => skipTelemetry(driver, logger, txn)), + mergeMap(() => getNumberOfItems(server, driver, layout, logger, txn)), mergeMap(async itemsCount => { const viewport = layout.getViewport(itemsCount); await Promise.all([ driver.setViewport(viewport, logger), - waitForVisualizations(server, driver, itemsCount, layout, logger), + waitForVisualizations(server, driver, itemsCount, layout, logger, txn), ]); }), mergeMap(async () => { // Waiting till _after_ elements have rendered before injecting our CSS // allows for them to be displayed properly in many cases - await injectCustomCss(driver, layout, logger); + await injectCustomCss(driver, layout, logger, txn); + const apmPositionElements = txn?.startSpan('position_elements', 'correction'); if (layout.positionElements) { // position panel elements for print layout await layout.positionElements(driver, logger); } + if (apmPositionElements) apmPositionElements.end(); - await waitForRenderComplete(driver, layout, captureConfig, logger); + await waitForRenderComplete(driver, layout, captureConfig, logger, txn); }), mergeMap(async () => { return await Promise.all([ - getTimeRange(driver, layout, logger), - getElementPositionAndAttributes(driver, layout, logger), + getTimeRange(driver, layout, logger, txn), + getElementPositionAndAttributes(driver, layout, logger, txn), ]).then(([timeRange, elementsPositionAndAttributes]) => ({ elementsPositionAndAttributes, timeRange, })); }), catchError(err => { + apm.captureError(err); logger.error(err); return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); }) @@ -85,7 +104,7 @@ export function screenshotsObservableFactory( const elements = data.elementsPositionAndAttributes ? data.elementsPositionAndAttributes : getDefaultElementPosition(layout.getViewport(1)); - const screenshots = await getScreenshots(driver, elements, logger); + const screenshots = await getScreenshots(driver, elements, logger, txn); const { timeRange, error: setupError } = data; return { timeRange, screenshots, error: setupError }; } @@ -96,7 +115,10 @@ export function screenshotsObservableFactory( ); }), take(urls.length), - toArray() + toArray(), + tap(() => { + if (txn) txn.end(); + }) ); }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts index fbae1f91a7a6a73..a2c72c25961c78a 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/open_url.ts @@ -5,20 +5,23 @@ */ import { i18n } from '@kbn/i18n'; -import { ConditionalHeaders, ServerFacade } from '../../../../types'; -import { LevelLogger } from '../../../../server/lib'; +import apm from 'elastic-apm-node'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; +import { LevelLogger } from '../../../../server/lib'; +import { ConditionalHeaders, ServerFacade } from '../../../../types'; import { PAGELOAD_SELECTOR } from '../../constants'; +import { ApmTransaction } from './types'; export const openUrl = async ( server: ServerFacade, browser: HeadlessBrowser, url: string, conditionalHeaders: ConditionalHeaders, - logger: LevelLogger + logger: LevelLogger, + txn: ApmTransaction ): Promise => { + const apmSpan = txn?.startSpan('open_url', 'wait'); const config = server.config(); - try { await browser.open( url, @@ -30,14 +33,13 @@ export const openUrl = async ( logger ); } catch (err) { + apm.captureError(err); 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 }, }) ); } + if (apmSpan) apmSpan.end(); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts index 1762a78f2272046..63f70fc96c5904c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts @@ -7,10 +7,16 @@ import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; import { LevelLogger } from '../../../../server/lib'; import { CONTEXT_SKIPTELEMETRY } from './constants'; +import { ApmTransaction } from './types'; const LAST_REPORT_STORAGE_KEY = 'xpack.data'; -export async function skipTelemetry(browser: HeadlessBrowser, logger: LevelLogger) { +export async function skipTelemetry( + browser: HeadlessBrowser, + logger: LevelLogger, + txn: ApmTransaction +) { + const apmSpan = txn?.startSpan('skip_telemetry', 'correction'); const storageData = await browser.evaluate( { fn: storageKey => { @@ -31,4 +37,6 @@ export async function skipTelemetry(browser: HeadlessBrowser, logger: LevelLogge ); logger.debug(`added data to localStorage to skip telmetry: ${storageData}`); + + if (apmSpan) apmSpan.end(); } diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts index ab81a952f345ce0..caec3ae628a9430 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/types.ts @@ -30,7 +30,7 @@ export interface ElementsPositionAndAttribute { } export interface Screenshot { - base64EncodedData: Buffer; + base64EncodedData: string; title: string; description: string; } @@ -46,3 +46,12 @@ export interface ScreenshotResults { screenshots: Screenshot[]; error?: Error; } + +export interface ApmSpan { + end: () => void; +} +interface ApmTransactionInitialized { + startSpan: (name: string, type: string) => ApmSpan | null; + end: () => void; +} +export type ApmTransaction = ApmTransactionInitialized | null; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts index 2f6dc2829dfd8de..1531c8c6224bdb3 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts @@ -10,13 +10,17 @@ import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/br import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_WAITFORRENDER } from './constants'; +import { ApmTransaction } from './types'; export const waitForRenderComplete = async ( browser: HeadlessBrowser, layout: LayoutInstance, captureConfig: CaptureConfig, - logger: LevelLogger + logger: LevelLogger, + txn: ApmTransaction ) => { + const apmSpan = txn?.startSpan('wait_for_render', 'wait'); + logger.debug( i18n.translate('xpack.reporting.screencapture.waitingForRenderComplete', { defaultMessage: 'waiting for rendering to complete', @@ -76,5 +80,7 @@ export const waitForRenderComplete = async ( defaultMessage: 'rendering is complete', }) ); + + if (apmSpan) apmSpan.end(); }); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts index 93ad40026dff81f..602f8bf82bd3948 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_visualizations.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import { i18n } from '@kbn/i18n'; import { ServerFacade } from '../../../../types'; import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers'; import { LevelLogger } from '../../../../server/lib'; import { LayoutInstance } from '../../layouts/layout'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; +import { ApmTransaction } from './types'; type SelectorArgs = Record; @@ -27,8 +29,10 @@ export const waitForVisualizations = async ( browser: HeadlessBrowser, itemsCount: number, layout: LayoutInstance, - logger: LevelLogger + logger: LevelLogger, + txn: ApmTransaction ): Promise => { + const apmSpan = txn?.startSpan('wait_for_visualizations', 'wait'); const config = server.config(); const { renderComplete: renderCompleteSelector } = layout.selectors; @@ -53,6 +57,7 @@ export const waitForVisualizations = async ( logger.debug(`found ${itemsCount} rendered elements in the DOM`); } catch (err) { + apm.captureError(err); throw new Error( i18n.translate('xpack.reporting.screencapture.couldntFinishRendering', { defaultMessage: `An error occurred when trying to wait for {count} visualizations to finish rendering. You may need to increase '{configKey}'. {error}`, @@ -64,4 +69,6 @@ export const waitForVisualizations = async ( }) ); } + + if (apmSpan) apmSpan.end(); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 8670f0027af89e4..7082725a8705b97 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import { ElasticsearchServiceSetup } from 'kibana/server'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; @@ -38,6 +39,10 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); return 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: any; + const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ server, job, logger })), @@ -46,6 +51,9 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut mergeMap(conditionalHeaders => { const urls = getFullUrls({ server, job }); const hashUrl = urls[0]; + if (apmGetAssets) apmGetAssets.end(); + + apmGeneratePng = apmTrans?.startSpan('generate_png_pipeline', 'execute'); return generatePngObservable( jobLogger, hashUrl, @@ -55,10 +63,12 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut ); }), map(({ buffer, warnings }) => { + if (apmGeneratePng) apmGeneratePng.end(); + return { content_type: 'image/png', - content: buffer.toString('base64'), - size: buffer.byteLength, + content: buffer, + size: buffer.length, warnings, }; }), diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index 88e91982adc6320..ddd38e38a83c421 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -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 { LevelLogger } from '../../../../server/lib'; @@ -25,12 +26,16 @@ export function generatePngObservableFactory( browserTimezone: string, conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams - ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { + ): Rx.Observable<{ buffer: string; 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$ = screenshotsObservable({ logger, urls: [url], @@ -39,6 +44,8 @@ export function generatePngObservableFactory( browserTimezone, }).pipe( map((results: ScreenshotResults[]) => { + if (apmScreenshots) apmScreenshots.end(); + return { buffer: results[0].screenshots[0].base64EncodedData, warnings: results.reduce((found, current) => { @@ -51,6 +58,7 @@ export function generatePngObservableFactory( }) ); + if (apmTrans) apmTrans.end(); return screenshots$; }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index 535c2dcd439a7af..d268e1ccaf1f737 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import { ElasticsearchServiceSetup } from 'kibana/server'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; @@ -39,6 +40,10 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut const logger = parentLogger.clone([PDF_JOB_TYPE, 'execute']); return 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: any; + const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ server, job, logger })), @@ -47,8 +52,10 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut mergeMap(conditionalHeaders => getCustomLogo({ reporting, server, job, conditionalHeaders })), mergeMap(({ logo, conditionalHeaders }) => { const urls = getFullUrls({ server, job }); - const { browserTimezone, layout, title } = job; + if (apmGetAssets) apmGetAssets.end(); + + apmGeneratePdf = apmTrans?.startSpan('generate_pdf_pipeline', 'execute'); return generatePdfObservable( jobLogger, title, @@ -59,12 +66,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); @@ -72,6 +87,8 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut ); const stop$ = Rx.fromEventPattern(cancellationToken.on); + + if (apmTrans) apmTrans.end(); return process$.pipe(takeUntil(stop$)).toPromise(); }; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index d78effaa1fc2f91..b5c3f210bc93f37 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import apm from 'elastic-apm-node'; import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; @@ -40,8 +41,14 @@ export function generatePdfObservableFactory( conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams, logo?: string - ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { + ): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { + const apmTrans = apm.startTransaction('reporting generate_pdf', 'reporting'); + const apmLayout = apmTrans?.startSpan('create_layout', 'setup'); + const layout = createLayout(server, layoutParams) as LayoutInstance; + if (apmLayout) apmLayout.end(); + + const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup'); const screenshots$ = screenshotsObservable({ logger, urls, @@ -50,27 +57,50 @@ export function generatePdfObservableFactory( browserTimezone, }).pipe( mergeMap(async (results: ScreenshotResults[]) => { - const pdfOutput = pdf.create(layout, logo); + if (apmScreenshots) apmScreenshots.end(); + const apmSetup = apmTrans?.startSpan('setup_pdf', 'setup'); + const pdfOutput = pdf.create(layout, logo); if (title) { const timeRange = getTimeRange(results); title += timeRange ? ` - ${timeRange.duration}` : ''; pdfOutput.setTitle(title); } + if (apmSetup) apmSetup.end(); results.forEach(r => { r.screenshots.forEach(screenshot => { + logger.debug( + `Adding image to PDF. Image base64 size: ${screenshot.base64EncodedData?.length || 0}` + ); // prettier-ignore + const apmAddImage = apmTrans?.startSpan('add_pdf_image', 'output'); pdfOutput.addImage(screenshot.base64EncodedData, { title: screenshot.title, description: screenshot.description, }); + if (apmAddImage) apmAddImage.end(); }); }); - pdfOutput.generate(); + let buffer: Buffer | null = null; + try { + const apmCompilePdf = apmTrans?.startSpan('compile_pdf', 'output'); + logger.debug(`Compiling PDF...`); + pdfOutput.generate(); + if (apmCompilePdf) apmCompilePdf.end(); + + const apmGetBuffer = apmTrans?.startSpan('get_buffer', 'output'); + logger.debug(`Generating PDF Buffer...`); + buffer = await pdfOutput.getBuffer(); + logger.debug(`PDF buffer byte length: ${buffer?.byteLength || 0}`); + if (apmGetBuffer) apmGetBuffer.end(); + } catch (err) { + apm.captureError(err); + logger.error(`Could not generate the PDF buffer! ${err}`); + } return { - buffer: await pdfOutput.getBuffer(), + buffer, warnings: results.reduce((found, current) => { if (current.error) { found.push(current.error.message); @@ -81,6 +111,7 @@ export function generatePdfObservableFactory( }) ); + if (apmTrans) apmTrans.end(); return screenshots$; }; } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 60799e3e918b82e..02d6b4dd7bcde45 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -183,7 +183,7 @@ export class HeadlessChromiumDriver { logger.info(`handled ${interceptedCount} page requests`); } - public async screenshot(elementPosition: ElementPosition): Promise { + public async screenshot(elementPosition: ElementPosition): Promise { let clip; if (elementPosition) { const { boundingClientRect, scroll = { x: 0, y: 0 } } = elementPosition; @@ -195,11 +195,7 @@ export class HeadlessChromiumDriver { }; } - const screenshot = await this.page.screenshot({ - clip, - }); - - return screenshot.toString('base64'); + return await this.page.screenshot({ clip }); } public async evaluate(