feature: report performance metrics for files plugin

This commit is contained in:
Baturalp Gurdin 2022-11-08 11:59:58 +01:00 committed by GitHub
parent 199eb170f9
commit 7b02bba8df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 13 deletions

View file

@ -15,6 +15,9 @@ import { Readable, Transform } from 'stream';
import { pipeline } from 'stream/promises';
import { promisify } from 'util';
import { lastValueFrom, defer } from 'rxjs';
import { PerformanceMetricEvent, reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { FilesPlugin } from '../../../plugin';
import { FILE_UPLOAD_PERFORMANCE_EVENT_NAME } from '../../../performance';
import type { BlobStorageClient } from '../../types';
import type { ReadableContentStream } from './content_stream';
import { getReadableContentStream, getWritableContentStream } from './content_stream';
@ -29,8 +32,14 @@ export const BLOB_STORAGE_SYSTEM_INDEX_NAME = '.kibana_blob_storage';
export const MAX_BLOB_STORE_SIZE_BYTES = 50 * 1024 * 1024 * 1024; // 50 GiB
interface UploadOptions {
transforms?: Transform[];
id?: string;
}
export class ElasticsearchBlobStorageClient implements BlobStorageClient {
private static defaultSemaphore: Semaphore;
/**
* Call this function once to globally set a concurrent upload limit for
* all {@link ElasticsearchBlobStorageClient} instances.
@ -89,14 +98,14 @@ export class ElasticsearchBlobStorageClient implements BlobStorageClient {
}
});
public async upload(
src: Readable,
{ transforms, id }: { transforms?: Transform[]; id?: string } = {}
): Promise<{ id: string; size: number }> {
public async upload(src: Readable, options: UploadOptions = {}) {
const { transforms, id } = options;
await this.createIndexIfNotExists();
const processUpload = async () => {
try {
const analytics = FilesPlugin.getAnalytics();
const dest = getWritableContentStream({
id,
client: this.esClient,
@ -106,14 +115,32 @@ export class ElasticsearchBlobStorageClient implements BlobStorageClient {
maxChunkSize: this.chunkSize,
},
});
await pipeline.apply(null, [src, ...(transforms ?? []), dest] as unknown as Parameters<
typeof pipeline
>);
return {
id: dest.getContentReferenceId()!,
size: dest.getBytesWritten(),
const start = performance.now();
await pipeline([src, ...(transforms ?? []), dest]);
const end = performance.now();
const _id = dest.getContentReferenceId()!;
const size = dest.getBytesWritten();
const perfArgs: PerformanceMetricEvent = {
eventName: FILE_UPLOAD_PERFORMANCE_EVENT_NAME,
duration: end - start,
key1: 'size',
value1: size,
meta: {
datasource: 'es',
id: _id,
index: this.index,
chunkSize: this.chunkSize,
},
};
if (analytics) {
reportPerformanceMetricEvent(analytics, perfArgs);
}
return { id: _id, size };
} catch (e) {
this.logger.error(`Could not write chunks to Elasticsearch for id ${id}: ${e}`);
throw e;

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { AnalyticsServiceStart } from '@kbn/core-analytics-server';
import type { JsonValue } from '@kbn/utility-types';
import type { Readable, Transform } from 'stream';
@ -29,6 +30,11 @@ export interface UploadOptions {
* And so on.
*/
id?: string;
/**
* Optional analytics service client in order to report performance of upload.
*/
analytics?: AnalyticsServiceStart;
}
/**

View file

@ -13,6 +13,7 @@ import cuid from 'cuid';
import { type Logger, SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { AuditLogger } from '@kbn/security-plugin/server';
import type { UsageCounter } from '@kbn/usage-collection-plugin/server';
import type {
File,
FileJSON,
@ -34,6 +35,11 @@ import { createAuditEvent } from '../audit_events';
import type { FileClient, CreateArgs, DeleteArgs, P1, ShareArgs } from './types';
import { serializeJSON, toJSON } from '../file/to_json';
import { createDefaultFileAttributes } from './utils';
import {
PerfArgs,
withReportPerformanceMetric,
FILE_DOWNLOAD_PERFORMANCE_EVENT_NAME,
} from '../performance';
export type UploadOptions = Omit<BlobUploadOptions, 'id'>;
@ -219,10 +225,21 @@ export class FileClientImpl implements FileClient {
});
};
public download: BlobStorageClient['download'] = (args) => {
public download: BlobStorageClient['download'] = async (args) => {
this.incrementUsageCounter('DOWNLOAD');
try {
return this.blobStorageClient.download(args);
const perf: PerfArgs = {
eventData: {
eventName: FILE_DOWNLOAD_PERFORMANCE_EVENT_NAME,
key1: 'size',
value1: args.size,
meta: {
id: args.id,
},
},
};
return withReportPerformanceMetric(perf, () => this.blobStorageClient.download(args));
} catch (e) {
this.incrementUsageCounter('DOWNLOAD_ERROR');
throw e;

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export const FILE_DOWNLOAD_PERFORMANCE_EVENT_NAME = 'file-download';
export const FILE_UPLOAD_PERFORMANCE_EVENT_NAME = 'file-upload';

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
export type { PerfArgs } from './report_performance';
export { withReportPerformanceMetric } from './report_performance';
export {
FILE_DOWNLOAD_PERFORMANCE_EVENT_NAME,
FILE_UPLOAD_PERFORMANCE_EVENT_NAME,
} from './event_names';

View file

@ -0,0 +1,32 @@
/*
* 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 { PerformanceMetricEvent, reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { Optional } from 'utility-types';
import { FilesPlugin } from '../plugin';
export interface PerfArgs {
eventData: Optional<PerformanceMetricEvent, 'duration'>;
}
export async function withReportPerformanceMetric<T>(perfArgs: PerfArgs, cb: () => Promise<T>) {
const analytics = FilesPlugin.getAnalytics();
const start = performance.now();
const response = await cb();
const end = performance.now();
if (analytics) {
reportPerformanceMetricEvent(analytics, {
...perfArgs.eventData,
duration: end - start,
});
}
return response;
}

View file

@ -14,6 +14,7 @@ import type {
CoreStart,
} from '@kbn/core/server';
import { AnalyticsServiceStart } from '@kbn/core/server';
import { PLUGIN_ID } from '../common/constants';
import {
setFileKindsRegistry,
@ -35,6 +36,7 @@ import { registerRoutes, registerFileKindRoutes } from './routes';
import { Counters, registerUsageCollector } from './usage';
export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSetupDependencies> {
private static analytics?: AnalyticsServiceStart;
private readonly logger: Logger;
private fileServiceFactory: undefined | FileServiceFactory;
private securitySetup: FilesPluginSetupDependencies['security'];
@ -44,6 +46,14 @@ export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSe
this.logger = initializerContext.logger.get();
}
public static getAnalytics() {
return this.analytics;
}
private static setAnalytics(analytics: AnalyticsServiceStart) {
this.analytics = analytics;
}
public setup(
core: CoreSetup,
{ security, usageCollection }: FilesPluginSetupDependencies
@ -89,8 +99,9 @@ export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSe
}
public start(coreStart: CoreStart, { security }: FilesPluginStartDependencies): FilesStart {
const { savedObjects } = coreStart;
const { savedObjects, analytics } = coreStart;
this.securityStart = security;
FilesPlugin.setAnalytics(analytics);
const esClient = coreStart.elasticsearch.client.asInternalUser;
const blobStorageService = new BlobStorageService(
esClient,