Skip to content

Commit

Permalink
[Files] Added usage counters for delete, unshare and download (#140091)
Browse files Browse the repository at this point in the history
* added usage counters for download, unshare and delete

* remove unused property

* also count share usages

* refactor how counters are formatted

* added more specific error type counters

* also record upload abort errors
  • Loading branch information
jloleysens authored Sep 21, 2022
1 parent f44f1d1 commit a160f0d
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 33 deletions.
63 changes: 49 additions & 14 deletions x-pack/plugins/files/server/file_client/file_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import moment from 'moment';
import { Readable } from 'stream';
import mimeType from 'mime';
import cuid from 'cuid';
import type { Logger } from '@kbn/core/server';
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,
Expand All @@ -23,6 +24,7 @@ import type {
BlobStorageClient,
UploadOptions as BlobUploadOptions,
} from '../blob_storage_service';
import { getCounters, Counters } from '../usage';
import { File as FileImpl } from '../file';
import { FileShareServiceStart, InternalFileShareService } from '../file_share_service';
import { enforceMaxByteSizeTransform } from './stream_transforms';
Expand Down Expand Up @@ -59,6 +61,15 @@ export function createFileClient({
}

export class FileClientImpl implements FileClient {
/**
* A usage counter instance that is shared across all FileClient instances.
*/
private static usageCounter: undefined | UsageCounter;

public static configureUsageCounter(uc: UsageCounter) {
FileClientImpl.usageCounter = uc;
}

private readonly logAuditEvent: AuditLogger['log'];

constructor(
Expand All @@ -78,6 +89,14 @@ export class FileClientImpl implements FileClient {
};
}

private getCounters() {
return getCounters(this.fileKind);
}

private incrementUsageCounter(counter: Counters) {
FileClientImpl.usageCounter?.incrementCounter({ counterName: this.getCounters()[counter] });
}

private instantiateFile<M = unknown>(id: string, metadata: FileMetadata<M>): File<M> {
return new FileImpl(
id,
Expand Down Expand Up @@ -144,19 +163,29 @@ export class FileClientImpl implements FileClient {
}

public async delete({ id, hasContent = true }: DeleteArgs) {
if (this.internalFileShareService) {
// Stop sharing this file
await this.internalFileShareService.deleteForFile({ id });
this.incrementUsageCounter('DELETE');
try {
if (this.internalFileShareService) {
// Stop sharing this file
await this.internalFileShareService.deleteForFile({ id });
}
if (hasContent) await this.blobStorageClient.delete(id);
await this.metadataClient.delete({ id });
this.logAuditEvent(
createAuditEvent({
action: 'delete',
outcome: 'success',
message: `Deleted file with "${id}"`,
})
);
} catch (e) {
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
this.incrementUsageCounter('DELETE_ERROR_NOT_FOUND');
} else {
this.incrementUsageCounter('DELETE_ERROR');
}
throw e;
}
if (hasContent) await this.blobStorageClient.delete(id);
await this.metadataClient.delete({ id });
this.logAuditEvent(
createAuditEvent({
action: 'delete',
outcome: 'success',
message: `Deleted file with "${id}"`,
})
);
}

public deleteContent: BlobStorageClient['delete'] = (arg) => {
Expand Down Expand Up @@ -191,7 +220,13 @@ export class FileClientImpl implements FileClient {
};

public download: BlobStorageClient['download'] = (args) => {
return this.blobStorageClient.download(args);
this.incrementUsageCounter('DOWNLOAD');
try {
return this.blobStorageClient.download(args);
} catch (e) {
this.incrementUsageCounter('DOWNLOAD_ERROR');
throw e;
}
};

async share({ file, name, validUntil }: ShareArgs): Promise<FileShareJSONWithToken> {
Expand Down
13 changes: 11 additions & 2 deletions x-pack/plugins/files/server/file_service/file_service_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import {
} from '@kbn/core/server';
import { SecurityPluginSetup } from '@kbn/security-plugin/server';

import type { File, FileJSON, FileMetadata } from '../../common';
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
import { File, FileJSON, FileMetadata } from '../../common';
import { fileObjectType, fileShareObjectType, hiddenTypes } from '../saved_objects';
import { BlobStorageService } from '../blob_storage_service';
import { FileClientImpl } from '../file_client/file_client';
import { InternalFileShareService } from '../file_share_service';
import {
CreateFileArgs,
Expand Down Expand Up @@ -132,8 +134,15 @@ export class FileServiceFactoryImpl implements FileServiceFactory {
/**
* This function can only called during Kibana's setup phase
*/
public static setup(savedObjectsSetup: SavedObjectsServiceSetup): void {
public static setup(
savedObjectsSetup: SavedObjectsServiceSetup,
usageCounter?: UsageCounter
): void {
savedObjectsSetup.registerType<FileMetadata<{}>>(fileObjectType);
savedObjectsSetup.registerType(fileShareObjectType);
if (usageCounter) {
FileClientImpl.configureUsageCounter(usageCounter);
InternalFileShareService.configureUsageCounter(usageCounter);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
SavedObjectsErrorHelpers,
} from '@kbn/core/server';
import { nodeBuilder, escapeKuery } from '@kbn/es-query';
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
import type {
Pagination,
FileShareJSON,
Expand All @@ -22,6 +23,7 @@ import type {
import { FILE_SO_TYPE } from '../../common/constants';
import type { File } from '../../common/types';
import { fileShareObjectType } from '../saved_objects';
import { getCounters, Counters } from '../usage';
import { generateShareToken } from './generate_share_token';
import { FileShareServiceStart } from './types';
import {
Expand Down Expand Up @@ -126,35 +128,67 @@ function validateCreateArgs({ validUntil }: CreateShareArgs): void {
* @internal
*/
export class InternalFileShareService implements FileShareServiceStart {
private static usageCounter: undefined | UsageCounter;

public static configureUsageCounter(uc: UsageCounter) {
InternalFileShareService.usageCounter = uc;
}

private readonly savedObjectsType = fileShareObjectType.name;

constructor(
private readonly savedObjects: SavedObjectsClientContract | ISavedObjectsRepository
) {}

private incrementUsageCounter(counter: Counters) {
InternalFileShareService.usageCounter?.incrementCounter({
counterName: getCounters('share_service')[counter],
});
}

public async share(args: CreateShareArgs): Promise<FileShareJSONWithToken> {
validateCreateArgs(args);
const { file, name, validUntil } = args;
const so = await this.savedObjects.create<FileShare>(
this.savedObjectsType,
{
created: new Date().toISOString(),
name,
valid_until: validUntil ? validUntil : Number(moment().add(30, 'days')),
token: generateShareToken(),
},
{
references: [{ name: file.data.name, id: file.data.id, type: FILE_SO_TYPE }],
}
);
this.incrementUsageCounter('SHARE');
try {
validateCreateArgs(args);
const { file, name, validUntil } = args;
const so = await this.savedObjects.create<FileShare>(
this.savedObjectsType,
{
created: new Date().toISOString(),
name,
valid_until: validUntil ? validUntil : Number(moment().add(30, 'days')),
token: generateShareToken(),
},
{
references: [{ name: file.data.name, id: file.data.id, type: FILE_SO_TYPE }],
}
);

return { ...toFileShareJSON(so), token: so.attributes.token };
return { ...toFileShareJSON(so), token: so.attributes.token };
} catch (e) {
if (e instanceof ExpiryDateInThePastError) {
this.incrementUsageCounter('SHARE_ERROR_EXPIRATION_IN_PAST');
} else if (SavedObjectsErrorHelpers.isForbiddenError(e)) {
this.incrementUsageCounter('SHARE_ERROR_FORBIDDEN');
} else if (SavedObjectsErrorHelpers.isConflictError(e)) {
this.incrementUsageCounter('SHARE_ERROR_CONFLICT');
} else {
this.incrementUsageCounter('SHARE_ERROR');
}
throw e;
}
}

public async delete({ id }: DeleteArgs): Promise<void> {
this.incrementUsageCounter('UNSHARE');
try {
await this.savedObjects.delete(this.savedObjectsType, id);
} catch (e) {
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
this.incrementUsageCounter('UNSHARE_ERROR_NOT_FOUND');
} else {
this.incrementUsageCounter('UNSHARE_ERROR');
}
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
throw new FileShareNotFoundError(`File share with id "${id}" not found.`);
}
Expand Down
8 changes: 6 additions & 2 deletions x-pack/plugins/files/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from './file_kinds_registry';
import type { FilesRequestHandlerContext, FilesRouter } from './routes/types';
import { registerRoutes } from './routes';
import { registerUsageCollector } from './usage';
import { Counters, registerUsageCollector } from './usage';

export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSetupDependencies> {
private readonly logger: Logger;
Expand All @@ -40,7 +40,8 @@ export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSe
core: CoreSetup,
{ security, usageCollection }: FilesPluginSetupDependencies
): FilesSetup {
FileServiceFactory.setup(core.savedObjects);
const usageCounter = usageCollection?.createUsageCounter(PLUGIN_ID);
FileServiceFactory.setup(core.savedObjects, usageCounter);
this.securitySetup = security;

core.http.registerRouteHandlerContext<FilesRequestHandlerContext, typeof PLUGIN_ID>(
Expand All @@ -51,6 +52,9 @@ export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSe
asCurrentUser: () => this.fileServiceFactory!.asScoped(req),
asInternalUser: () => this.fileServiceFactory!.asInternal(),
logger: this.logger.get('files-routes'),
usageCounter: usageCounter
? (counter: Counters) => usageCounter.incrementCounter({ counterName: counter })
: undefined,
},
};
}
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/files/server/routes/file_kind/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const handler: FileKindsRequestHandler<Params, unknown, Body> = async (
) {
return res.badRequest({ body: { message: e.message } });
} else if (e instanceof fileErrors.AbortedUploadError) {
fileService.usageCounter?.('UPLOAD_ERROR_ABORT');
fileService.logger.error(e);
return res.customError({ body: { message: e.message }, statusCode: 499 });
}
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/files/server/routes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import type {
Logger,
} from '@kbn/core/server';
import type { FileServiceStart } from '../file_service';
import { Counters } from '../usage';

export interface FilesRequestHandlerContext extends RequestHandlerContext {
files: Promise<{
fileService: {
asCurrentUser: () => FileServiceStart;
asInternalUser: () => FileServiceStart;
logger: Logger;
usageCounter?: (counter: Counters) => void;
};
}>;
}
Expand Down
31 changes: 31 additions & 0 deletions x-pack/plugins/files/server/usage/counters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export function getCounters(fileKind: string) {
return {
DELETE: `delete:${fileKind}`,
DELETE_ERROR: `delete:error:unknown:${fileKind}`,
DELETE_ERROR_NOT_FOUND: `delete:error:not_found:${fileKind}`,

SHARE: `share:${fileKind}`,
SHARE_ERROR: `share:error:unknown:${fileKind}`,
SHARE_ERROR_EXPIRATION_IN_PAST: `share:error:expiration_in_past:${fileKind}`,
SHARE_ERROR_FORBIDDEN: `share:error:forbidden:${fileKind}`,
SHARE_ERROR_CONFLICT: `share:error:conflict:${fileKind}`,

UNSHARE: `unshare:${fileKind}`,
UNSHARE_ERROR: `unshare:error:unknown:${fileKind}`,
UNSHARE_ERROR_NOT_FOUND: `unshare:error:not_found:${fileKind}`,

DOWNLOAD: `download:${fileKind}`,
DOWNLOAD_ERROR: `download:error:unknown:${fileKind}`,

UPLOAD_ERROR_ABORT: `upload:error:abort:${fileKind}`,
};
}

export type Counters = keyof ReturnType<typeof getCounters>;
2 changes: 2 additions & 0 deletions x-pack/plugins/files/server/usage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
*/

export { registerUsageCollector } from './register_usage_collector';
export type { Counters } from './counters';
export { getCounters } from './counters';

0 comments on commit a160f0d

Please sign in to comment.