From 5a8b7afe0bf89c094707fabdc5fd26457346632a Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 10 Jan 2024 09:58:34 -0700 Subject: [PATCH] [Reporting ] run task function simplifications of image export types (#174410) ## Summary This PR cleans up extra layers of abstraction in image export types that could complicate progress of the proposal of "auto" timeouts. See https://github.com/elastic/kibana/issues/131852 ## Changes Minor code cleanup of the "runTask" functions of export types that create screenshots, by removing the `generatePdf*` / `generatePng` helper callback functions and inlining the work those modules were doing. The helper modules were an integral part of the screenshotting pipelines, but in the unit tests they were completely mocked. Now that we have a proper mock utility of the `screenshotting` plugin start contract, we no longer need mockable code in a separate layer of the pipelines. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../export_types/pdf/generate_pdf.ts | 58 ---------- .../export_types/pdf/generate_pdf_v2.ts | 75 ------------- .../export_types/pdf/printable_pdf.test.ts | 42 ++++---- .../export_types/pdf/printable_pdf.ts | 60 +++++++---- .../export_types/pdf/printable_pdf_v2.test.ts | 77 +++++++------ .../export_types/pdf/printable_pdf_v2.ts | 101 ++++++++++-------- .../export_types/png/generate_png.ts | 73 ------------- .../export_types/png/png_v2.test.ts | 69 ++++++------ .../kbn-reporting/export_types/png/png_v2.ts | 71 ++++++++---- .../export_types/png/tsconfig.json | 1 - 10 files changed, 251 insertions(+), 376 deletions(-) delete mode 100644 packages/kbn-reporting/export_types/pdf/generate_pdf.ts delete mode 100644 packages/kbn-reporting/export_types/pdf/generate_pdf_v2.ts delete mode 100644 packages/kbn-reporting/export_types/png/generate_png.ts diff --git a/packages/kbn-reporting/export_types/pdf/generate_pdf.ts b/packages/kbn-reporting/export_types/pdf/generate_pdf.ts deleted file mode 100644 index 5703e16e48abc..0000000000000 --- a/packages/kbn-reporting/export_types/pdf/generate_pdf.ts +++ /dev/null @@ -1,58 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Observable } from 'rxjs'; -import { mergeMap, tap } from 'rxjs/operators'; - -import type { PdfScreenshotOptions, PdfScreenshotResult } from '@kbn/screenshotting-plugin/server'; -import { getTracker } from './pdf_tracker'; - -interface PdfResult { - buffer: Uint8Array | null; - metrics?: PdfScreenshotResult['metrics']; - warnings: string[]; -} - -type GetScreenshotsFn = (options: PdfScreenshotOptions) => Observable; - -export function generatePdfObservable( - getScreenshots: GetScreenshotsFn, - options: PdfScreenshotOptions -): Observable { - const tracker = getTracker(); - tracker.startScreenshots(); - - return getScreenshots(options).pipe( - tap(({ metrics }) => { - if (metrics.cpu) { - tracker.setCpuUsage(metrics.cpu); - } - if (metrics.memory) { - tracker.setMemoryUsage(metrics.memory); - } - }), - mergeMap(async ({ data: buffer, errors, metrics, renderErrors }) => { - tracker.endScreenshots(); - const warnings: string[] = []; - if (errors) { - warnings.push(...errors.map((error) => error.message)); - } - if (renderErrors) { - warnings.push(...renderErrors); - } - - tracker.end(); - - return { - buffer, - metrics, - warnings, - }; - }) - ); -} diff --git a/packages/kbn-reporting/export_types/pdf/generate_pdf_v2.ts b/packages/kbn-reporting/export_types/pdf/generate_pdf_v2.ts deleted file mode 100644 index 54489c6258bd8..0000000000000 --- a/packages/kbn-reporting/export_types/pdf/generate_pdf_v2.ts +++ /dev/null @@ -1,75 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Observable } from 'rxjs'; -import { mergeMap, tap } from 'rxjs/operators'; - -import type { LocatorParams, ReportingServerInfo } from '@kbn/reporting-common/types'; -import type { TaskPayloadPDFV2 } from '@kbn/reporting-export-types-pdf-common'; -import type { PdfScreenshotOptions, PdfScreenshotResult } from '@kbn/screenshotting-plugin/server'; -import type { UrlOrUrlWithContext } from '@kbn/screenshotting-plugin/server/screenshots'; -import { type ReportingConfigType, getFullRedirectAppUrl } from '@kbn/reporting-server'; - -import { getTracker } from './pdf_tracker'; - -interface PdfResult { - buffer: Uint8Array | null; - metrics?: PdfScreenshotResult['metrics']; - warnings: string[]; -} - -type GetScreenshotsFn = (options: PdfScreenshotOptions) => Observable; - -export function generatePdfObservableV2( - config: ReportingConfigType, - serverInfo: ReportingServerInfo, - getScreenshots: GetScreenshotsFn, - job: TaskPayloadPDFV2, - locatorParams: LocatorParams[], - options: Omit -): Observable { - const tracker = getTracker(); - tracker.startScreenshots(); - - /** - * For each locator we get the relative URL to the redirect app - */ - const urls = locatorParams.map((locator) => [ - getFullRedirectAppUrl(config, serverInfo, job.spaceId, job.forceNow), - locator, - ]) as unknown as UrlOrUrlWithContext[]; - - const screenshots$ = getScreenshots({ ...options, urls }).pipe( - tap(({ metrics }) => { - if (metrics.cpu) { - tracker.setCpuUsage(metrics.cpu); - } - if (metrics.memory) { - tracker.setMemoryUsage(metrics.memory); - } - }), - mergeMap(async ({ data: buffer, errors, metrics, renderErrors }) => { - tracker.endScreenshots(); - const warnings: string[] = []; - if (errors) { - warnings.push(...errors.map((error) => error.message)); - } - if (renderErrors) { - warnings.push(...renderErrors); - } - - return { - buffer, - metrics, - warnings, - }; - }) - ); - - return screenshots$; -} diff --git a/packages/kbn-reporting/export_types/pdf/printable_pdf.test.ts b/packages/kbn-reporting/export_types/pdf/printable_pdf.test.ts index 9ca0fa34effcd..10c21ede18227 100644 --- a/packages/kbn-reporting/export_types/pdf/printable_pdf.test.ts +++ b/packages/kbn-reporting/export_types/pdf/printable_pdf.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; +import * as Rx from 'rxjs'; import { Writable } from 'stream'; import { coreMock, elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; @@ -14,12 +14,8 @@ import { CancellationToken } from '@kbn/reporting-common'; import { TaskPayloadPDF } from '@kbn/reporting-export-types-pdf-common'; import { createMockConfigSchema } from '@kbn/reporting-mocks-server'; import { cryptoFactory } from '@kbn/reporting-server'; -import { ScreenshottingStart } from '@kbn/screenshotting-plugin/server'; - +import { createMockScreenshottingStart } from '@kbn/screenshotting-plugin/server/mock'; import { PdfV1ExportType } from '.'; -import { generatePdfObservable } from './generate_pdf'; - -jest.mock('./generate_pdf'); let content: string; let mockPdfExportType: PdfV1ExportType; @@ -34,6 +30,9 @@ const encryptHeaders = async (headers: Record) => { return await crypto.encrypt(headers); }; +const screenshottingMock = createMockScreenshottingStart(); +const getScreenshotsSpy = jest.spyOn(screenshottingMock, 'getScreenshots'); +const testContent = 'raw string from get_screenhots'; const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPDF; beforeEach(async () => { @@ -54,15 +53,20 @@ beforeEach(async () => { esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: mockCoreStart.savedObjects, uiSettings: mockCoreStart.uiSettings, - screenshotting: {} as unknown as ScreenshottingStart, + screenshotting: screenshottingMock, + }); + getScreenshotsSpy.mockImplementation(() => { + return Rx.of({ + metrics: { cpu: 0, pages: 1 }, + data: Buffer.from(testContent), + errors: [], + renderErrors: [], + }); }); }); -afterEach(() => (generatePdfObservable as jest.Mock).mockReset()); - test(`passes browserTimezone to generatePdf`, async () => { const encryptedHeaders = await encryptHeaders({}); - (generatePdfObservable as jest.Mock).mockReturnValue(of({ buffer: Buffer.from('') })); const browserTimezone = 'UTC'; await mockPdfExportType.runTask( @@ -76,17 +80,20 @@ test(`passes browserTimezone to generatePdf`, async () => { stream ); - expect(generatePdfObservable).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ browserTimezone: 'UTC' }) - ); + expect(getScreenshotsSpy).toHaveBeenCalledWith({ + browserTimezone: 'UTC', + format: 'pdf', + headers: {}, + layout: undefined, + logo: false, + title: undefined, + urls: ['http://localhost:80/mock-server-basepath/app/kibana#/something'], + }); }); test(`returns content_type of application/pdf`, async () => { const encryptedHeaders = await encryptHeaders({}); - (generatePdfObservable as jest.Mock).mockReturnValue(of({ buffer: Buffer.from('') })); - const { content_type: contentType } = await mockPdfExportType.runTask( 'pdfJobId', getBasePayload({ objects: [], headers: encryptedHeaders }), @@ -97,9 +104,6 @@ test(`returns content_type of application/pdf`, async () => { }); test(`returns content of generatePdf getBuffer base64 encoded`, async () => { - const testContent = 'test content'; - (generatePdfObservable as jest.Mock).mockReturnValue(of({ buffer: Buffer.from(testContent) })); - const encryptedHeaders = await encryptHeaders({}); await mockPdfExportType.runTask( 'pdfJobId', diff --git a/packages/kbn-reporting/export_types/pdf/printable_pdf.ts b/packages/kbn-reporting/export_types/pdf/printable_pdf.ts index 6c8ce2a5b1d0d..6aaf0e5c491e2 100644 --- a/packages/kbn-reporting/export_types/pdf/printable_pdf.ts +++ b/packages/kbn-reporting/export_types/pdf/printable_pdf.ts @@ -29,10 +29,10 @@ import { } from '@kbn/reporting-export-types-pdf-common'; import { ExportType, decryptJobHeaders } from '@kbn/reporting-server'; -import { generatePdfObservable } from './generate_pdf'; -import { validateUrls } from './validate_urls'; import { getCustomLogo } from './get_custom_logo'; import { getFullUrls } from './get_full_urls'; +import { getTracker } from './pdf_tracker'; +import { validateUrls } from './validate_urls'; /** * @deprecated @@ -76,15 +76,15 @@ export class PdfV1ExportType extends ExportType { - const jobLogger = this.logger.get(`execute-job:${jobId}`); + const logger = this.logger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; const process$: Observable = of(1).pipe( - mergeMap(() => decryptJobHeaders(this.config.encryptionKey, job.headers, jobLogger)), + mergeMap(() => decryptJobHeaders(this.config.encryptionKey, job.headers, logger)), mergeMap(async (headers) => { - const fakeRequest = this.getFakeRequest(headers, job.spaceId, jobLogger); + const fakeRequest = this.getFakeRequest(headers, job.spaceId, logger); const uiSettingsClient = await this.getUiSettingsClient(fakeRequest); return getCustomLogo(uiSettingsClient, headers); }), @@ -96,18 +96,11 @@ export class PdfV1ExportType extends ExportType - this.startDeps.screenshotting!.getScreenshots({ - format: 'pdf', - title, - logo, - urls, - browserTimezone, - headers, - layout, - }), - { + const tracker = getTracker(); + tracker.startScreenshots(); + + return this.startDeps + .screenshotting!.getScreenshots({ format: 'pdf', title, logo, @@ -115,8 +108,35 @@ export class PdfV1ExportType extends ExportType { + if (metrics.cpu) { + tracker.setCpuUsage(metrics.cpu); + } + if (metrics.memory) { + tracker.setMemoryUsage(metrics.memory); + } + }), + mergeMap(async ({ data: buffer, errors, metrics, renderErrors }) => { + tracker.endScreenshots(); + const warnings: string[] = []; + if (errors) { + warnings.push(...errors.map((error) => error.message)); + } + if (renderErrors) { + warnings.push(...renderErrors); + } + + tracker.end(); + + return { + buffer, + metrics, + warnings, + }; + }) + ); }), tap(({ buffer }) => { apmGeneratePdf?.end(); @@ -130,7 +150,7 @@ export class PdfV1ExportType extends ExportType { - jobLogger.error(err); + logger.error(err); return throwError(err); }) ); diff --git a/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.test.ts b/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.test.ts index 4e4f0c1491130..28d28ade31e97 100644 --- a/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.test.ts +++ b/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; -import type { Writable } from 'stream'; +import * as Rx from 'rxjs'; +import { Writable } from 'stream'; import { coreMock, elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks'; import { CancellationToken } from '@kbn/reporting-common'; @@ -15,12 +15,8 @@ import type { LocatorParams } from '@kbn/reporting-common/types'; import type { TaskPayloadPDFV2 } from '@kbn/reporting-export-types-pdf-common'; import { createMockConfigSchema } from '@kbn/reporting-mocks-server'; import { cryptoFactory } from '@kbn/reporting-server'; -import type { ScreenshottingStart } from '@kbn/screenshotting-plugin/server'; - +import { createMockScreenshottingStart } from '@kbn/screenshotting-plugin/server/mock'; import { PdfExportType } from '.'; -import { generatePdfObservableV2 } from './generate_pdf_v2'; - -jest.mock('./generate_pdf_v2'); let content: string; let mockPdfExportType: PdfExportType; @@ -34,7 +30,11 @@ const encryptHeaders = async (headers: Record) => { const crypto = cryptoFactory(mockEncryptionKey); return await crypto.encrypt(headers); }; +let encryptedHeaders: string; +const screenshottingMock = createMockScreenshottingStart(); +const getScreenshotsSpy = jest.spyOn(screenshottingMock, 'getScreenshots'); +const testContent = 'raw string from get_screenhots'; const getBasePayload = (baseObj: any) => ({ params: { forceNow: 'test' }, @@ -50,8 +50,10 @@ beforeEach(async () => { const mockCoreSetup = coreMock.createSetup(); const mockCoreStart = coreMock.createStart(); - mockPdfExportType = new PdfExportType(mockCoreSetup, configType, mockLogger, context); + encryptedHeaders = await encryptHeaders({}); + + mockPdfExportType = new PdfExportType(mockCoreSetup, configType, mockLogger, context); mockPdfExportType.setup({ basePath: { set: jest.fn() }, }); @@ -59,21 +61,26 @@ beforeEach(async () => { esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: mockCoreStart.savedObjects, uiSettings: mockCoreStart.uiSettings, - screenshotting: {} as unknown as ScreenshottingStart, + screenshotting: screenshottingMock, }); -}); -afterEach(() => (generatePdfObservableV2 as jest.Mock).mockReset()); + getScreenshotsSpy.mockImplementation(() => { + return Rx.of({ + metrics: { cpu: 0, pages: 1 }, + data: Buffer.from(testContent), + errors: [], + renderErrors: [], + }); + }); +}); test(`passes browserTimezone to generatePdf`, async () => { - const encryptedHeaders = await encryptHeaders({}); - (generatePdfObservableV2 as jest.Mock).mockReturnValue(of(Buffer.from(''))); - const browserTimezone = 'UTC'; await mockPdfExportType.runTask( 'pdfJobId', getBasePayload({ forceNow: 'test', + layout: { dimensions: {} }, title: 'PDF Params Timezone Test', locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], browserTimezone, @@ -83,24 +90,30 @@ test(`passes browserTimezone to generatePdf`, async () => { stream ); - expect(generatePdfObservableV2).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.objectContaining({ browserTimezone: 'UTC' }) - ); + expect(getScreenshotsSpy).toHaveBeenCalledWith({ + browserTimezone: 'UTC', + format: 'pdf', + headers: {}, + layout: { dimensions: {} }, + logo: false, + title: 'PDF Params Timezone Test', + urls: [ + [ + 'http://localhost:80/mock-server-basepath/app/reportingRedirect?forceNow=test', + { __REPORTING_REDIRECT_LOCATOR_STORE_KEY__: { id: 'test', version: 'test' } }, + ], + ], + }); }); test(`returns content_type of application/pdf`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - (generatePdfObservableV2 as jest.Mock).mockReturnValue(of({ buffer: Buffer.from('') })); - const { content_type: contentType } = await mockPdfExportType.runTask( 'pdfJobId', - getBasePayload({ locatorParams: [], headers: encryptedHeaders }), + getBasePayload({ + layout: { dimensions: {} }, + locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], + headers: encryptedHeaders, + }), cancellationToken, stream ); @@ -108,13 +121,13 @@ test(`returns content_type of application/pdf`, async () => { }); test(`returns content of generatePdf getBuffer base64 encoded`, async () => { - const testContent = 'test content'; - (generatePdfObservableV2 as jest.Mock).mockReturnValue(of({ buffer: Buffer.from(testContent) })); - - const encryptedHeaders = await encryptHeaders({}); await mockPdfExportType.runTask( 'pdfJobId', - getBasePayload({ locatorParams: [], headers: encryptedHeaders }), + getBasePayload({ + layout: { dimensions: {} }, + locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], + headers: encryptedHeaders, + }), cancellationToken, stream ); diff --git a/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.ts b/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.ts index cc975c536803d..24e445e503d51 100644 --- a/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.ts +++ b/packages/kbn-reporting/export_types/pdf/printable_pdf_v2.ts @@ -22,17 +22,18 @@ import { REPORTING_REDIRECT_LOCATOR_STORE_KEY, REPORTING_TRANSACTION_TYPE, } from '@kbn/reporting-common'; -import type { TaskRunResult, UrlOrUrlLocatorTuple } from '@kbn/reporting-common/types'; +import type { TaskRunResult } from '@kbn/reporting-common/types'; +import type { TaskPayloadPDFV2 } from '@kbn/reporting-export-types-pdf-common'; import { JobParamsPDFV2, PDF_JOB_TYPE_V2, PDF_REPORT_TYPE_V2, - TaskPayloadPDFV2, } from '@kbn/reporting-export-types-pdf-common'; -import { decryptJobHeaders, getFullRedirectAppUrl, ExportType } from '@kbn/reporting-server'; +import { ExportType, decryptJobHeaders, getFullRedirectAppUrl } from '@kbn/reporting-server'; +import type { UrlOrUrlWithContext } from '@kbn/screenshotting-plugin/server/screenshots'; -import { generatePdfObservableV2 } from './generate_pdf_v2'; import { getCustomLogo } from './get_custom_logo'; +import { getTracker } from './pdf_tracker'; export class PdfExportType extends ExportType { id = PDF_REPORT_TYPE_V2; @@ -80,66 +81,82 @@ export class PdfExportType extends ExportType cancellationToken: CancellationToken, stream: Writable ) => { - const jobLogger = this.logger.get(`execute-job:${jobId}`); + const logger = this.logger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf-v2', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; const { encryptionKey } = this.config; const process$: Rx.Observable = of(1).pipe( - mergeMap(() => decryptJobHeaders(encryptionKey, payload.headers, jobLogger)), + mergeMap(() => decryptJobHeaders(encryptionKey, payload.headers, logger)), mergeMap(async (headers: Headers) => { - const fakeRequest = this.getFakeRequest(headers, payload.spaceId, jobLogger); + const fakeRequest = this.getFakeRequest(headers, payload.spaceId, logger); const uiSettingsClient = await this.getUiSettingsClient(fakeRequest); return await getCustomLogo(uiSettingsClient, headers); }), mergeMap(({ logo, headers }) => { const { browserTimezone, layout, title, locatorParams } = payload; - let urls: UrlOrUrlLocatorTuple[]; - if (locatorParams) { - urls = locatorParams.map((locator) => [ - getFullRedirectAppUrl( - this.config, - this.getServerInfo(), - payload.spaceId, - payload.forceNow - ), - locator, - ]); - } apmGetAssets?.end(); apmGeneratePdf = apmTrans.startSpan('generate-pdf-pipeline', 'execute'); - return generatePdfObservableV2( - this.config, - this.getServerInfo(), - () => - this.startDeps.screenshotting!.getScreenshots({ - format: 'pdf', - title, - logo, - browserTimezone, - headers, - layout, - urls: urls.map((url) => - typeof url === 'string' - ? url - : [url[0], { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: url[1] }] - ), - }), - payload, - locatorParams, - { + const tracker = getTracker(); + tracker.startScreenshots(); + + /** + * For each locator we get the relative URL to the redirect app + */ + const urls = locatorParams.map((locator) => [ + getFullRedirectAppUrl( + this.config, + this.getServerInfo(), + payload.spaceId, + payload.forceNow + ), + locator, + ]) as unknown as UrlOrUrlWithContext[]; + + return this.startDeps + .screenshotting!.getScreenshots({ format: 'pdf', title, logo, browserTimezone, headers, layout, - } - ); + urls: urls.map((url) => + typeof url === 'string' + ? url + : [url[0], { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: url[1] }] + ), + }) + .pipe( + tap(({ metrics }) => { + if (metrics.cpu) { + tracker.setCpuUsage(metrics.cpu); + } + if (metrics.memory) { + tracker.setMemoryUsage(metrics.memory); + } + }), + mergeMap(async ({ data: buffer, errors, metrics, renderErrors }) => { + tracker.endScreenshots(); + const warnings: string[] = []; + if (errors) { + warnings.push(...errors.map((error) => error.message)); + } + if (renderErrors) { + warnings.push(...renderErrors); + } + + return { + buffer, + metrics, + warnings, + }; + }) + ); }), tap(({ buffer }) => { apmGeneratePdf?.end(); @@ -154,7 +171,7 @@ export class PdfExportType extends ExportType warnings, })), catchError((err) => { - jobLogger.error(err); + logger.error(err); return Rx.throwError(() => err); }) ); diff --git a/packages/kbn-reporting/export_types/png/generate_png.ts b/packages/kbn-reporting/export_types/png/generate_png.ts deleted file mode 100644 index d0ce9b9791690..0000000000000 --- a/packages/kbn-reporting/export_types/png/generate_png.ts +++ /dev/null @@ -1,73 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import apm from 'elastic-apm-node'; -import { Observable } from 'rxjs'; -import { finalize, map, tap } from 'rxjs/operators'; - -import type { Logger } from '@kbn/logging'; -import { REPORTING_TRANSACTION_TYPE } from '@kbn/reporting-common/constants'; -import type { PngScreenshotOptions, PngScreenshotResult } from '@kbn/screenshotting-plugin/server'; - -interface PngResult { - buffer: Buffer; - metrics?: PngScreenshotResult['metrics']; - warnings: string[]; -} - -type GetScreenshotsFn = (options: PngScreenshotOptions) => Observable; - -export function generatePngObservable( - getScreenshots: GetScreenshotsFn, - logger: Logger, - options: Omit -): Observable { - const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE); - if (!options.layout?.dimensions) { - throw new Error(`LayoutParams.Dimensions is undefined.`); - } - - const apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', 'setup'); - let apmBuffer: typeof apm.currentSpan; - - return getScreenshots({ - ...options, - format: 'png', - layout: { id: 'preserve_layout', ...options.layout }, - }).pipe( - tap(({ metrics }) => { - if (metrics) { - apmTrans.setLabel('cpu', metrics.cpu, false); - apmTrans.setLabel('memory', metrics.memory, false); - } - apmScreenshots?.end(); - apmBuffer = apmTrans.startSpan('get-buffer', 'output') ?? null; - }), - map(({ metrics, results }) => ({ - metrics, - buffer: results[0].screenshots[0].data, - warnings: results.reduce((found, current) => { - if (current.error) { - found.push(current.error.message); - } - if (current.renderErrors) { - found.push(...current.renderErrors); - } - return found; - }, [] as string[]), - })), - tap(({ buffer }) => { - logger.debug(`PNG buffer byte length: ${buffer.byteLength}`); - apmTrans.setLabel('byte-length', buffer.byteLength, false); - }), - finalize(() => { - apmBuffer?.end(); - apmTrans.end(); - }) - ); -} diff --git a/packages/kbn-reporting/export_types/png/png_v2.test.ts b/packages/kbn-reporting/export_types/png/png_v2.test.ts index a6cc8b1891eef..bd59306af8168 100644 --- a/packages/kbn-reporting/export_types/png/png_v2.test.ts +++ b/packages/kbn-reporting/export_types/png/png_v2.test.ts @@ -15,12 +15,9 @@ import type { LocatorParams } from '@kbn/reporting-common/types'; import type { TaskPayloadPNGV2 } from '@kbn/reporting-export-types-png-common'; import { createMockConfigSchema } from '@kbn/reporting-mocks-server'; import { cryptoFactory } from '@kbn/reporting-server'; -import type { ScreenshottingStart } from '@kbn/screenshotting-plugin/server'; - +import { createMockScreenshottingStart } from '@kbn/screenshotting-plugin/server/mock'; +import type { CaptureResult } from '@kbn/screenshotting-plugin/server/screenshots'; import { PngExportType } from '.'; -import { generatePngObservable } from './generate_png'; - -jest.mock('./generate_png'); let content: string; let mockPngExportType: PngExportType; @@ -34,49 +31,51 @@ const encryptHeaders = async (headers: Record) => { const crypto = cryptoFactory(mockEncryptionKey); return await crypto.encrypt(headers); }; +let encryptedHeaders: string; +const screenshottingMock = createMockScreenshottingStart(); +const getScreenshotsSpy = jest.spyOn(screenshottingMock, 'getScreenshots'); +const testContent = 'raw string from get_screenhots'; const getBasePayload = (baseObj: unknown) => baseObj as TaskPayloadPNGV2; beforeEach(async () => { content = ''; stream = { write: jest.fn((chunk) => (content += chunk)) } as unknown as typeof stream; - const configType = createMockConfigSchema({ - encryptionKey: mockEncryptionKey, - queue: { - indexInterval: 'daily', - timeout: Infinity, - }, - }); - + const configType = createMockConfigSchema({ encryptionKey: mockEncryptionKey }); const context = coreMock.createPluginInitializerContext(configType); const mockCoreSetup = coreMock.createSetup(); const mockCoreStart = coreMock.createStart(); + encryptedHeaders = await encryptHeaders({}); + mockPngExportType = new PngExportType(mockCoreSetup, configType, mockLogger, context); mockPngExportType.setup({ basePath: { set: jest.fn() }, }); mockPngExportType.start({ + esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: mockCoreStart.savedObjects, uiSettings: mockCoreStart.uiSettings, - screenshotting: {} as unknown as ScreenshottingStart, - esClient: elasticsearchServiceMock.createClusterClient(), + screenshotting: screenshottingMock, }); -}); -afterEach(() => (generatePngObservable as jest.Mock).mockReset()); + getScreenshotsSpy.mockImplementation(() => { + return Rx.of({ + metrics: { cpu: 0 }, + results: [{ screenshots: [{ data: Buffer.from(testContent) }] }] as CaptureResult['results'], + }); + }); +}); test(`passes browserTimezone to generatePng`, async () => { - const encryptedHeaders = await encryptHeaders({}); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') })); - const browserTimezone = 'UTC'; await mockPngExportType.runTask( 'pngJobId', getBasePayload({ forceNow: 'test', + layout: { dimensions: {} }, locatorParams: [], browserTimezone, headers: encryptedHeaders, @@ -85,25 +84,24 @@ test(`passes browserTimezone to generatePng`, async () => { stream ); - expect(generatePngObservable).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ - browserTimezone: 'UTC', - headers: {}, - layout: { id: 'preserve_layout' }, - }) - ); + expect(getScreenshotsSpy).toHaveBeenCalledWith({ + format: 'png', + headers: {}, + layout: { dimensions: {}, id: 'preserve_layout' }, + urls: [ + [ + 'http://localhost:80/mock-server-basepath/app/reportingRedirect?forceNow=test', + { __REPORTING_REDIRECT_LOCATOR_STORE_KEY__: undefined }, + ], + ], + }); }); test(`returns content_type of application/png`, async () => { - const encryptedHeaders = await encryptHeaders({}); - - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('foo') })); - const { content_type: contentType } = await mockPngExportType.runTask( 'pngJobId', getBasePayload({ + layout: { dimensions: {} }, locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], headers: encryptedHeaders, }), @@ -114,13 +112,10 @@ test(`returns content_type of application/png`, async () => { }); test(`returns content of generatePng getBuffer base64 encoded`, async () => { - const testContent = 'raw string from get_screenhots'; - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - - const encryptedHeaders = await encryptHeaders({}); await mockPngExportType.runTask( 'pngJobId', getBasePayload({ + layout: { dimensions: {} }, locatorParams: [{ version: 'test', id: 'test' }] as LocatorParams[], headers: encryptedHeaders, }), diff --git a/packages/kbn-reporting/export_types/png/png_v2.ts b/packages/kbn-reporting/export_types/png/png_v2.ts index 15f0c26d45932..2b824bde18a0b 100644 --- a/packages/kbn-reporting/export_types/png/png_v2.ts +++ b/packages/kbn-reporting/export_types/png/png_v2.ts @@ -38,9 +38,7 @@ import { PNG_REPORT_TYPE_V2, TaskPayloadPNGV2, } from '@kbn/reporting-export-types-png-common'; -import { decryptJobHeaders, getFullRedirectAppUrl, ExportType } from '@kbn/reporting-server'; - -import { generatePngObservable } from './generate_png'; +import { decryptJobHeaders, ExportType, getFullRedirectAppUrl } from '@kbn/reporting-server'; export class PngExportType extends ExportType { id = PNG_REPORT_TYPE_V2; @@ -88,14 +86,14 @@ export class PngExportType extends ExportType cancellationToken: CancellationToken, stream: Writable ) => { - const jobLogger = this.logger.get(`execute-job:${jobId}`); + const logger = this.logger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf-v2', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; const { encryptionKey } = this.config; const process$: Observable = of(1).pipe( - mergeMap(() => decryptJobHeaders(encryptionKey, payload.headers, jobLogger)), + mergeMap(() => decryptJobHeaders(encryptionKey, payload.headers, logger)), mergeMap((headers) => { const url = getFullRedirectAppUrl( this.config, @@ -108,22 +106,57 @@ export class PngExportType extends ExportType apmGetAssets?.end(); apmGeneratePng = apmTrans.startSpan('generate-png-pipeline', 'execute'); + const options = { + headers, + browserTimezone: payload.browserTimezone, + layout: { ...payload.layout, id: 'preserve_layout' as const }, + }; - return generatePngObservable( - () => - this.startDeps.screenshotting!.getScreenshots({ - format: 'png', - headers, - layout: { ...payload.layout, id: 'preserve_layout' }, - urls: [[url, { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: locatorParams }]], - }), - jobLogger, - { + if (!options.layout?.dimensions) { + throw new Error(`LayoutParams.Dimensions is undefined.`); + } + + const apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', 'setup'); + let apmBuffer: typeof apm.currentSpan; + + return this.startDeps + .screenshotting!.getScreenshots({ + format: 'png', headers, - browserTimezone: payload.browserTimezone, layout: { ...payload.layout, id: 'preserve_layout' }, - } - ); + urls: [[url, { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: locatorParams }]], + }) + .pipe( + tap(({ metrics }) => { + if (metrics) { + apmTrans.setLabel('cpu', metrics.cpu, false); + apmTrans.setLabel('memory', metrics.memory, false); + } + apmScreenshots?.end(); + apmBuffer = apmTrans.startSpan('get-buffer', 'output') ?? null; + }), + map(({ metrics, results }) => ({ + metrics, + buffer: results[0].screenshots[0].data, + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + if (current.renderErrors) { + found.push(...current.renderErrors); + } + return found; + }, [] as string[]), + })), + tap(({ buffer }) => { + logger.debug(`PNG buffer byte length: ${buffer.byteLength}`); + apmTrans.setLabel('byte-length', buffer.byteLength, false); + }), + finalize(() => { + apmBuffer?.end(); + apmTrans.end(); + }) + ); }), tap(({ buffer }) => stream.write(buffer)), map(({ metrics, warnings }) => ({ @@ -131,7 +164,7 @@ export class PngExportType extends ExportType metrics: { png: metrics }, warnings, })), - tap({ error: (error) => jobLogger.error(error) }), + tap({ error: (error) => logger.error(error) }), finalize(() => apmGeneratePng?.end()) ); diff --git a/packages/kbn-reporting/export_types/png/tsconfig.json b/packages/kbn-reporting/export_types/png/tsconfig.json index bac09188079b5..b8a141a320dd8 100644 --- a/packages/kbn-reporting/export_types/png/tsconfig.json +++ b/packages/kbn-reporting/export_types/png/tsconfig.json @@ -23,6 +23,5 @@ "@kbn/reporting-export-types-png-common", "@kbn/core", "@kbn/reporting-mocks-server", - "@kbn/logging", ] }