Add per SO type telemetry to core usage counters (#181063)

## Summary

Tackles https://github.com/elastic/kibana/issues/180366

Leverages existing `'core'` _Usage Counters_ to add per SO type
telemetry for the HTTP SO API calls.
This commit is contained in:
Gerard Soldevila 2024-04-26 13:14:38 +02:00 committed by GitHub
parent 7bb7f21426
commit 9d01ab1ee8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 516 additions and 172 deletions

View file

@ -58,30 +58,30 @@ export const registerBulkCreateRoute = (
),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'post',
path: '/api/saved_objects/_bulk_create',
req,
request,
logger,
});
const { overwrite } = req.query;
const { overwrite } = request.query;
const types = [...new Set(request.body.map(({ type }) => type))];
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsBulkCreate({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsBulkCreate({ request, types }).catch(() => {});
const { savedObjects } = await context.core;
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
if (!allowHttpApiAccess) {
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
throwIfAnyTypeNotVisibleByAPI(types, savedObjects.typeRegistry);
}
const result = await savedObjects.client.bulkCreate(req.body, {
const result = await savedObjects.client.bulkCreate(request.body, {
overwrite,
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
return response.ok({ body: result });
})
);
};

View file

@ -43,25 +43,26 @@ export const registerBulkDeleteRoute = (
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'post',
path: '/api/saved_objects/_bulk_delete',
req,
request,
logger,
});
const { force } = req.query;
const { force } = request.query;
const types = [...new Set(request.body.map(({ type }) => type))];
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsBulkDelete({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsBulkDelete({ request, types }).catch(() => {});
const { savedObjects } = await context.core;
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
if (!allowHttpApiAccess) {
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
throwIfAnyTypeNotVisibleByAPI(types, savedObjects.typeRegistry);
}
const statuses = await savedObjects.client.bulkDelete(req.body, { force });
return res.ok({ body: statuses });
const statuses = await savedObjects.client.bulkDelete(request.body, { force });
return response.ok({ body: statuses });
})
);
};

View file

@ -42,25 +42,27 @@ export const registerBulkGetRoute = (
),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'post',
path: '/api/saved_objects/_bulk_get',
req,
request,
logger,
});
const types = [...new Set(request.body.map(({ type }) => type))];
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsBulkGet({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsBulkGet({ request, types }).catch(() => {});
const { savedObjects } = await context.core;
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
if (!allowHttpApiAccess) {
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
throwIfAnyTypeNotVisibleByAPI(types, savedObjects.typeRegistry);
}
const result = await savedObjects.client.bulkGet(req.body, {
const result = await savedObjects.client.bulkGet(request.body, {
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
return response.ok({ body: result });
})
);
};

View file

@ -40,25 +40,26 @@ export const registerBulkResolveRoute = (
),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'post',
path: '/api/saved_objects/_bulk_resolve',
req,
request,
logger,
});
const types = [...new Set(request.body.map(({ type }) => type))];
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsBulkResolve({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsBulkResolve({ request, types }).catch(() => {});
const { savedObjects } = await context.core;
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
if (!allowHttpApiAccess) {
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
throwIfAnyTypeNotVisibleByAPI(types, savedObjects.typeRegistry);
}
const result = await savedObjects.client.bulkResolve(req.body, {
const result = await savedObjects.client.bulkResolve(request.body, {
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
return response.ok({ body: result });
})
);
};

View file

@ -52,24 +52,25 @@ export const registerBulkUpdateRoute = (
),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'put',
path: '/api/saved_objects/_bulk_update',
req,
request,
logger,
});
const types = [...new Set(request.body.map(({ type }) => type))];
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsBulkUpdate({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsBulkUpdate({ request, types }).catch(() => {});
const { savedObjects } = await context.core;
const typesToCheck = [...new Set(req.body.map(({ type }) => type))];
if (!allowHttpApiAccess) {
throwIfAnyTypeNotVisibleByAPI(typesToCheck, savedObjects.typeRegistry);
throwIfAnyTypeNotVisibleByAPI(types, savedObjects.typeRegistry);
}
const savedObject = await savedObjects.client.bulkUpdate(req.body);
return res.ok({ body: savedObject });
const savedObject = await savedObjects.client.bulkUpdate(request.body);
return response.ok({ body: savedObject });
})
);
};

View file

@ -57,15 +57,15 @@ export const registerCreateRoute = (
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'post',
path: '/api/saved_objects/{type}/{id?}',
req,
request,
logger,
});
const { type, id } = req.params;
const { overwrite } = req.query;
const { type, id } = request.params;
const { overwrite } = request.query;
const {
attributes,
migrationVersion,
@ -73,10 +73,10 @@ export const registerCreateRoute = (
typeMigrationVersion,
references,
initialNamespaces,
} = req.body;
} = request.body;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsCreate({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsCreate({ request, types: [type] }).catch(() => {});
const { savedObjects } = await context.core;
if (!allowHttpApiAccess) {
@ -93,7 +93,7 @@ export const registerCreateRoute = (
migrationVersionCompatibility: 'compatible' as const,
};
const result = await savedObjects.client.create(type, attributes, options);
return res.ok({ body: result });
return response.ok({ body: result });
})
);
};

View file

@ -41,25 +41,25 @@ export const registerDeleteRoute = (
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'delete',
path: '/api/saved_objects/{type}/{id}',
req,
request,
logger,
});
const { type, id } = req.params;
const { force } = req.query;
const { type, id } = request.params;
const { force } = request.query;
const { getClient, typeRegistry } = (await context.core).savedObjects;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsDelete({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsDelete({ request, types: [type] }).catch(() => {});
if (!allowHttpApiAccess) {
throwIfTypeNotVisibleByAPI(type, typeRegistry);
}
const client = getClient();
const result = await client.delete(type, id, { force });
return res.ok({ body: result });
return response.ok({ body: result });
})
);
};

View file

@ -164,20 +164,20 @@ export const registerExportRoute = (
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
const cleaned = cleanOptions(req.body);
catchAndReturnBoomErrors(async (context, request, response) => {
const cleaned = cleanOptions(request.body);
const { typeRegistry, getExporter, getClient } = (await context.core).savedObjects;
const supportedTypes = typeRegistry.getImportableAndExportableTypes().map((t) => t.name);
let options: EitherExportOptions;
try {
options = validateOptions(cleaned, {
request: req,
request,
exportSizeLimit: maxImportExportSize,
supportedTypes,
});
} catch (e) {
return res.badRequest({
return response.badRequest({
body: e,
});
}
@ -191,7 +191,11 @@ export const registerExportRoute = (
const usageStatsClient = coreUsageData.getClient();
usageStatsClient
.incrementSavedObjectsExport({ request: req, types: cleaned.types, supportedTypes })
.incrementSavedObjectsExport({
request,
types: cleaned.types ?? [],
supportedTypes,
})
.catch(() => {});
try {
@ -207,7 +211,7 @@ export const registerExportRoute = (
createConcatStream([]),
]);
return res.ok({
return response.ok({
body: docsToExport.join('\n'),
headers: {
'Content-Disposition': `attachment; filename="export.ndjson"`,
@ -216,7 +220,7 @@ export const registerExportRoute = (
});
} catch (e) {
if (e instanceof SavedObjectsExportError) {
return res.badRequest({
return response.badRequest({
body: {
message: e.message,
attributes: e.attributes,

View file

@ -62,20 +62,22 @@ export const registerFindRoute = (
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'get',
path: '/api/saved_objects/_find',
req,
request,
logger,
});
const query = req.query;
const query = request.query;
const types: string[] = Array.isArray(query.type) ? query.type : [query.type];
const namespaces =
typeof req.query.namespaces === 'string' ? [req.query.namespaces] : req.query.namespaces;
typeof request.query.namespaces === 'string'
? [request.query.namespaces]
: request.query.namespaces;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsFind({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsFind({ request, types }).catch(() => {});
// manually validate to avoid using JSON.parse twice
let aggs;
@ -83,7 +85,7 @@ export const registerFindRoute = (
try {
aggs = JSON.parse(query.aggs);
} catch (e) {
return res.badRequest({
return response.badRequest({
body: {
message: 'invalid aggs value',
},
@ -93,9 +95,7 @@ export const registerFindRoute = (
const { savedObjects } = await context.core;
// check if registered type(s)are exposed to the global SO Http API's.
const findForTypes = Array.isArray(query.type) ? query.type : [query.type];
const unsupportedTypes = [...new Set(findForTypes)].filter((tname) => {
const unsupportedTypes = [...new Set(types)].filter((tname) => {
const fullType = savedObjects.typeRegistry.getType(tname);
// pass unknown types through to the registry to handle
if (!fullType?.hidden && fullType?.hiddenFromHttpApis) {
@ -109,7 +109,7 @@ export const registerFindRoute = (
const result = await savedObjects.client.find({
perPage: query.per_page,
page: query.page,
type: findForTypes,
type: types,
search: query.search,
defaultSearchOperator: query.default_search_operator,
searchFields:
@ -126,7 +126,7 @@ export const registerFindRoute = (
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
return response.ok({ body: result });
})
);
};

View file

@ -38,17 +38,17 @@ export const registerGetRoute = (
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'get',
path: '/api/saved_objects/{type}/{id}',
req,
request,
logger,
});
const { type, id } = req.params;
const { type, id } = request.params;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsGet({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsGet({ request, types: [type] }).catch(() => {});
const { savedObjects } = await context.core;
@ -59,7 +59,7 @@ export const registerGetRoute = (
const object = await savedObjects.client.get(type, id, {
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: object });
return response.ok({ body: object });
})
);
};

View file

@ -66,31 +66,31 @@ export const registerImportRoute = (
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
const { overwrite, createNewCopies, compatibilityMode } = req.query;
catchAndReturnBoomErrors(async (context, request, response) => {
const { overwrite, createNewCopies, compatibilityMode } = request.query;
const { getClient, getImporter, typeRegistry } = (await context.core).savedObjects;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient
.incrementSavedObjectsImport({
request: req,
request,
createNewCopies,
overwrite,
compatibilityMode,
})
.catch(() => {});
const file = req.body.file as FileStream;
const file = request.body.file as FileStream;
const fileExtension = extname(file.hapi.filename).toLowerCase();
if (fileExtension !== '.ndjson') {
return res.badRequest({ body: `Invalid file extension ${fileExtension}` });
return response.badRequest({ body: `Invalid file extension ${fileExtension}` });
}
let readStream: Readable;
try {
readStream = await createSavedObjectsStreamFromNdJson(file);
} catch (e) {
return res.badRequest({
return response.badRequest({
body: e,
});
}
@ -112,10 +112,10 @@ export const registerImportRoute = (
compatibilityMode,
});
return res.ok({ body: result });
return response.ok({ body: result });
} catch (e) {
if (e instanceof SavedObjectsImportError) {
return res.badRequest({
return response.badRequest({
body: {
message: e.message,
attributes: e.attributes,

View file

@ -33,22 +33,24 @@ export const registerLegacyExportRoute = (
tags: ['api'],
},
},
async (ctx, req, res) => {
async (context, request, response) => {
logger.warn(
"The export dashboard API '/api/kibana/dashboards/export' is deprecated. Use the saved objects export objects API '/api/saved_objects/_export' instead."
);
const ids = Array.isArray(req.query.dashboard) ? req.query.dashboard : [req.query.dashboard];
const { client } = (await ctx.core).savedObjects;
const ids = Array.isArray(request.query.dashboard)
? request.query.dashboard
: [request.query.dashboard];
const { client } = (await context.core).savedObjects;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementLegacyDashboardsExport({ request: req }).catch(() => {});
usageStatsClient.incrementLegacyDashboardsExport({ request }).catch(() => {});
const exported = await exportDashboards(ids, client, kibanaVersion);
const filename = `kibana-dashboards.${moment.utc().format('YYYY-MM-DD-HH-mm-ss')}.json`;
const body = JSON.stringify(exported, null, ' ');
return res.ok({
return response.ok({
body,
headers: {
'Content-Disposition': `attachment; filename="${filename}"`,

View file

@ -43,23 +43,23 @@ export const registerLegacyImportRoute = (
},
},
},
async (ctx, req, res) => {
async (context, request, response) => {
logger.warn(
"The import dashboard API '/api/kibana/dashboards/import' is deprecated. Use the saved objects import objects API '/api/saved_objects/_import' instead."
);
const { client } = (await ctx.core).savedObjects;
const objects = req.body.objects as SavedObject[];
const { force, exclude } = req.query;
const { client } = (await context.core).savedObjects;
const objects = request.body.objects as SavedObject[];
const { force, exclude } = request.query;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementLegacyDashboardsImport({ request: req }).catch(() => {});
usageStatsClient.incrementLegacyDashboardsImport({ request }).catch(() => {});
const result = await importDashboards(client, objects, {
overwrite: force,
exclude: Array.isArray(exclude) ? exclude : [exclude],
});
return res.ok({
return response.ok({
body: result,
});
}

View file

@ -34,25 +34,25 @@ export const registerResolveRoute = (
}),
},
},
router.handleLegacyErrors(async (context, req, res) => {
router.handleLegacyErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'get',
path: '/api/saved_objects/resolve/{type}/{id}',
req,
request,
logger,
});
const { type, id } = req.params;
const { type, id } = request.params;
const { savedObjects } = await context.core;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsResolve({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsResolve({ request, types: [type] }).catch(() => {});
if (!allowHttpApiAccess) {
throwIfTypeNotVisibleByAPI(type, savedObjects.typeRegistry);
}
const result = await savedObjects.client.resolve(type, id, {
migrationVersionCompatibility: 'compatible',
});
return res.ok({ body: result });
return response.ok({ body: result });
})
);
};

View file

@ -53,15 +53,15 @@ export const registerUpdateRoute = (
}),
},
},
catchAndReturnBoomErrors(async (context, req, res) => {
catchAndReturnBoomErrors(async (context, request, response) => {
logWarnOnExternalRequest({
method: 'get',
path: '/api/saved_objects/{type}/{id}',
req,
request,
logger,
});
const { type, id } = req.params;
const { attributes, version, references, upsert } = req.body;
const { type, id } = request.params;
const { attributes, version, references, upsert } = request.body;
const options: SavedObjectsUpdateOptions = {
version,
references,
@ -70,13 +70,13 @@ export const registerUpdateRoute = (
};
const usageStatsClient = coreUsageData.getClient();
usageStatsClient.incrementSavedObjectsUpdate({ request: req }).catch(() => {});
usageStatsClient.incrementSavedObjectsUpdate({ request, types: [type] }).catch(() => {});
const { savedObjects } = await context.core;
if (!allowHttpApiAccess) {
throwIfTypeNotVisibleByAPI(type, savedObjects.typeRegistry);
}
const result = await savedObjects.client.update(type, id, attributes, options);
return res.ok({ body: result });
return response.ok({ body: result });
})
);
};

View file

@ -381,7 +381,7 @@ describe('logWarnOnExternalRequest', () => {
logWarnOnExternalRequest({
method: 'get',
path: '/resolve/{type}/{id}',
req: extRequest,
request: extRequest,
logger,
});
expect(logger.warn).toHaveBeenCalledTimes(1);
@ -394,7 +394,7 @@ describe('logWarnOnExternalRequest', () => {
logWarnOnExternalRequest({
method: 'post',
path: '/_bulk_resolve',
req: extRequest,
request: extRequest,
logger,
});
expect(logger.warn).toHaveBeenCalledTimes(1);
@ -407,14 +407,14 @@ describe('logWarnOnExternalRequest', () => {
logWarnOnExternalRequest({
method: 'get',
path: '/resolve/{type}/{id}',
req: kibRequest,
request: kibRequest,
logger,
});
expect(logger.warn).toHaveBeenCalledTimes(0);
logWarnOnExternalRequest({
method: 'post',
path: '/_bulk_resolve',
req: kibRequest,
request: kibRequest,
logger,
});
expect(logger.warn).toHaveBeenCalledTimes(0);

View file

@ -171,7 +171,7 @@ export function isKibanaRequest({ headers }: KibanaRequest) {
export interface LogWarnOnExternalRequest {
method: string;
path: string;
req: KibanaRequest;
request: KibanaRequest;
logger: Logger;
}
@ -181,8 +181,8 @@ export interface LogWarnOnExternalRequest {
* @internal
*/
export function logWarnOnExternalRequest(params: LogWarnOnExternalRequest) {
const { method, path, req, logger } = params;
if (!isKibanaRequest(req)) {
const { method, path, request, logger } = params;
if (!isKibanaRequest(request)) {
logger.warn(`The ${method} saved object API ${path} is deprecated.`);
}
}

View file

@ -12,6 +12,7 @@ import type { CoreUsageStats } from '@kbn/core-usage-data-server';
/** @internal */
export interface BaseIncrementOptions {
request: KibanaRequest;
types?: string[]; // we might not have info on the imported types for some operations, e.g. for import we're using a readStream
}
/** @internal */
@ -29,7 +30,6 @@ export type IncrementSavedObjectsResolveImportErrorsOptions = BaseIncrementOptio
/** @internal */
export type IncrementSavedObjectsExportOptions = BaseIncrementOptions & {
types?: string[];
supportedTypes: string[];
};

View file

@ -34,6 +34,8 @@ import type {
CoreIncrementUsageCounter,
ConfigUsageData,
CoreConfigUsageData,
CoreIncrementCounterParams,
CoreUsageCounter,
} from '@kbn/core-usage-data-server';
import {
CORE_USAGE_STATS_TYPE,
@ -493,28 +495,33 @@ export class CoreUsageDataService
typeRegistry.registerType(coreUsageStatsType);
};
this.coreUsageStatsClient = new CoreUsageStatsClient(
(message: string) => this.logger.debug(message),
http.basePath,
internalRepositoryPromise,
this.stop$
);
const registerUsageCounter = (usageCounter: CoreUsageCounter) => {
this.incrementUsageCounter = (params) => usageCounter.incrementCounter(params);
};
const incrementUsageCounter = (params: CoreIncrementCounterParams) => {
try {
this.incrementUsageCounter(params);
} catch (e) {
// Self-defense mechanism since the handler is externally registered
this.logger.debug('Failed to increase the usage counter');
this.logger.debug(e);
}
};
this.coreUsageStatsClient = new CoreUsageStatsClient({
debugLogger: (message: string) => this.logger.debug(message),
basePath: http.basePath,
repositoryPromise: internalRepositoryPromise,
stop$: this.stop$,
incrementUsageCounter,
});
const contract: InternalCoreUsageDataSetup = {
registerType,
getClient: () => this.coreUsageStatsClient!,
registerUsageCounter: (usageCounter) => {
this.incrementUsageCounter = (params) => usageCounter.incrementCounter(params);
},
incrementUsageCounter: (params) => {
try {
this.incrementUsageCounter(params);
} catch (e) {
// Self-defense mechanism since the handler is externally registered
this.logger.debug('Failed to increase the usage counter');
this.logger.debug(e);
}
},
registerUsageCounter,
incrementUsageCounter,
};
return contract;

View file

@ -40,18 +40,20 @@ import { CoreUsageStatsClient } from '.';
describe('CoreUsageStatsClient', () => {
const stop$ = new Subject<void>();
const incrementUsageCounterMock = jest.fn();
const setup = (namespace?: string) => {
const debugLoggerMock = jest.fn();
const basePathMock = httpServiceMock.createBasePath();
// we could mock a return value for basePathMock.get, but it isn't necessary for testing purposes
basePathMock.remove.mockReturnValue(namespace ? `/s/${namespace}` : '/');
const repositoryMock = savedObjectsRepositoryMock.create();
const usageStatsClient = new CoreUsageStatsClient(
debugLoggerMock,
basePathMock,
Promise.resolve(repositoryMock),
stop$
);
const usageStatsClient = new CoreUsageStatsClient({
debugLogger: debugLoggerMock,
basePath: basePathMock,
repositoryPromise: Promise.resolve(repositoryMock),
stop$,
incrementUsageCounter: incrementUsageCounterMock,
});
return { usageStatsClient, debugLoggerMock, basePathMock, repositoryMock };
};
const firstPartyRequestHeaders = {
@ -65,6 +67,10 @@ describe('CoreUsageStatsClient', () => {
jest.useFakeTimers();
});
beforeEach(() => {
incrementUsageCounterMock.mockReset();
});
afterEach(() => {
stop$.next();
});
@ -280,6 +286,23 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsBulkCreate({
request: httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }),
types: ['type1', 'type2'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_CREATE_STATS_PREFIX}.kibanaRequest.yes.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_CREATE_STATS_PREFIX}.kibanaRequest.yes.types.type2`,
});
});
});
describe('#incrementSavedObjectsBulkGet', () => {
@ -368,6 +391,23 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsBulkGet({
request: httpServerMock.createKibanaRequest(),
types: ['type1', 'type2'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_GET_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_GET_STATS_PREFIX}.kibanaRequest.no.types.type2`,
});
});
});
describe('#incrementSavedObjectsBulkResolve', () => {
@ -456,6 +496,23 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsBulkResolve({
request: httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }),
types: ['type1', 'type2'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_RESOLVE_STATS_PREFIX}.kibanaRequest.yes.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_RESOLVE_STATS_PREFIX}.kibanaRequest.yes.types.type2`,
});
});
});
describe('#incrementSavedObjectsBulkUpdate', () => {
@ -544,6 +601,23 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsBulkUpdate({
request: httpServerMock.createKibanaRequest(),
types: ['type1', 'type2'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_UPDATE_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_UPDATE_STATS_PREFIX}.kibanaRequest.no.types.type2`,
});
});
});
describe('#incrementSavedObjectsCreate', () => {
@ -629,6 +703,20 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsCreate({
request: httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }),
types: ['type1'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(1);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${CREATE_STATS_PREFIX}.kibanaRequest.yes.types.type1`,
});
});
});
describe('#incrementSavedObjectsBulkDelete', () => {
@ -717,6 +805,23 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsBulkDelete({
request: httpServerMock.createKibanaRequest(),
types: ['type1', 'type2'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_DELETE_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${BULK_DELETE_STATS_PREFIX}.kibanaRequest.no.types.type2`,
});
});
});
describe('#incrementSavedObjectsDelete', () => {
@ -802,6 +907,20 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsDelete({
request: httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }),
types: ['type1'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(1);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${DELETE_STATS_PREFIX}.kibanaRequest.yes.types.type1`,
});
});
});
describe('#incrementSavedObjectsFind', () => {
@ -881,6 +1000,20 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsFind({
request: httpServerMock.createKibanaRequest(),
types: ['type1'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(1);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${FIND_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
});
});
describe('#incrementSavedObjectsGet', () => {
@ -960,6 +1093,20 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsGet({
request: httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }),
types: ['type1'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(1);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${GET_STATS_PREFIX}.kibanaRequest.yes.types.type1`,
});
});
});
describe('#incrementSavedObjectsResolve', () => {
@ -1048,6 +1195,20 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsResolve({
request: httpServerMock.createKibanaRequest(),
types: ['type1'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(1);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${RESOLVE_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
});
});
describe('#incrementSavedObjectsUpdate', () => {
@ -1133,6 +1294,20 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsUpdate({
request: httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }),
types: ['type1'],
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(1);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${UPDATE_STATS_PREFIX}.kibanaRequest.yes.types.type1`,
});
});
});
describe('#incrementSavedObjectsImport', () => {
@ -1255,6 +1430,23 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage if provided', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsImport({
request: httpServerMock.createKibanaRequest(),
types: ['type1', 'type2'],
} as IncrementSavedObjectsImportOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${IMPORT_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${IMPORT_STATS_PREFIX}.kibanaRequest.no.types.type2`,
});
});
});
describe('#incrementSavedObjectsResolveImportErrors', () => {
@ -1386,6 +1578,23 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage if provided', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsResolveImportErrors({
request: httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }),
types: ['type1', 'type2'],
} as IncrementSavedObjectsImportOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes.types.type2`,
});
});
});
describe('#incrementSavedObjectsExport', () => {
@ -1478,6 +1687,24 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementSavedObjectsExport({
request: httpServerMock.createKibanaRequest(),
types: ['type1', 'type2'],
supportedTypes: ['type1', 'type2', 'type3'],
} as IncrementSavedObjectsExportOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${EXPORT_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${EXPORT_STATS_PREFIX}.kibanaRequest.no.types.type2`,
});
});
});
describe('#incrementLegacyDashboardsImport', () => {
@ -1489,7 +1716,7 @@ describe('CoreUsageStatsClient', () => {
await expect(
usageStatsClient.incrementLegacyDashboardsImport({
request,
} as IncrementSavedObjectsExportOptions)
} as BaseIncrementOptions)
).resolves.toBeUndefined();
await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
@ -1501,7 +1728,7 @@ describe('CoreUsageStatsClient', () => {
const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders });
await usageStatsClient.incrementLegacyDashboardsImport({
request,
} as IncrementSavedObjectsExportOptions);
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
@ -1528,7 +1755,7 @@ describe('CoreUsageStatsClient', () => {
const request = httpServerMock.createKibanaRequest();
await usageStatsClient.incrementLegacyDashboardsImport({
request,
} as IncrementSavedObjectsExportOptions);
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
@ -1548,6 +1775,23 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage if provided', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementLegacyDashboardsImport({
request: httpServerMock.createKibanaRequest(),
types: ['type1', 'type2'],
} as IncrementSavedObjectsImportOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${LEGACY_DASHBOARDS_IMPORT_STATS_PREFIX}.kibanaRequest.no.types.type2`,
});
});
});
describe('#incrementLegacyDashboardsExport', () => {
@ -1559,7 +1803,7 @@ describe('CoreUsageStatsClient', () => {
await expect(
usageStatsClient.incrementLegacyDashboardsExport({
request,
} as IncrementSavedObjectsExportOptions)
} as BaseIncrementOptions)
).resolves.toBeUndefined();
await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
@ -1571,7 +1815,7 @@ describe('CoreUsageStatsClient', () => {
const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders });
await usageStatsClient.incrementLegacyDashboardsExport({
request,
} as IncrementSavedObjectsExportOptions);
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
@ -1598,7 +1842,7 @@ describe('CoreUsageStatsClient', () => {
const request = httpServerMock.createKibanaRequest();
await usageStatsClient.incrementLegacyDashboardsExport({
request,
} as IncrementSavedObjectsExportOptions);
} as BaseIncrementOptions);
await jest.runOnlyPendingTimersAsync();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
@ -1618,5 +1862,22 @@ describe('CoreUsageStatsClient', () => {
incrementOptions
);
});
it('reports SO type usage if provided', async () => {
const { usageStatsClient } = setup('foo');
await usageStatsClient.incrementLegacyDashboardsExport({
request: httpServerMock.createKibanaRequest(),
types: ['type1', 'type2'],
} as IncrementSavedObjectsImportOptions);
await jest.runOnlyPendingTimersAsync();
expect(incrementUsageCounterMock).toHaveBeenCalledTimes(2);
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.kibanaRequest.no.types.type1`,
});
expect(incrementUsageCounterMock).toHaveBeenCalledWith({
counterName: `savedObjects.${LEGACY_DASHBOARDS_EXPORT_STATS_PREFIX}.kibanaRequest.no.types.type2`,
});
});
});
});

View file

@ -12,7 +12,7 @@ import type {
SavedObjectsIncrementCounterField,
} from '@kbn/core-saved-objects-api-server';
import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
import type { CoreUsageStats } from '@kbn/core-usage-data-server';
import type { CoreUsageStats, CoreIncrementCounterParams } from '@kbn/core-usage-data-server';
import {
type ICoreUsageStatsClient,
type BaseIncrementOptions,
@ -24,6 +24,7 @@ import {
REPOSITORY_RESOLVE_OUTCOME_STATS,
} from '@kbn/core-usage-data-base-server-internal';
import {
type Observable,
bufferWhen,
exhaustMap,
filter,
@ -33,6 +34,7 @@ import {
skip,
Subject,
takeUntil,
tap,
} from 'rxjs';
export const BULK_CREATE_STATS_PREFIX = 'apiCalls.savedObjectsBulkCreate';
@ -95,18 +97,46 @@ const SPACE_CONTEXT_REGEX = /^\/s\/([a-z0-9_\-]+)/;
const MAX_BUFFER_SIZE = 10_000;
const DEFAULT_BUFFER_TIME_MS = 10_000;
/**
* Interface that models some of the core events (e.g. SO HTTP API calls)
* @internal
*/
export interface CoreUsageEvent {
id: string;
isKibanaRequest: boolean;
types?: string[];
}
/** @internal */
export interface CoreUsageStatsClientParams {
debugLogger: (message: string) => void;
basePath: IBasePath;
repositoryPromise: Promise<ISavedObjectsRepository>;
stop$: Observable<void>;
incrementUsageCounter: (params: CoreIncrementCounterParams) => void;
bufferTimeMs?: number;
}
/** @internal */
export class CoreUsageStatsClient implements ICoreUsageStatsClient {
private readonly debugLogger: (message: string) => void;
private readonly basePath: IBasePath;
private readonly repositoryPromise: Promise<ISavedObjectsRepository>;
private readonly fieldsToIncrement$ = new Subject<string[]>();
private readonly flush$ = new Subject<void>();
private readonly coreUsageEvents$ = new Subject<CoreUsageEvent>();
constructor(
private readonly debugLogger: (message: string) => void,
private readonly basePath: IBasePath,
private readonly repositoryPromise: Promise<ISavedObjectsRepository>,
stop$: Subject<void>,
bufferTimeMs: number = DEFAULT_BUFFER_TIME_MS
) {
constructor({
debugLogger,
basePath,
repositoryPromise,
stop$,
incrementUsageCounter,
bufferTimeMs = DEFAULT_BUFFER_TIME_MS,
}: CoreUsageStatsClientParams) {
this.debugLogger = debugLogger;
this.basePath = basePath;
this.repositoryPromise = repositoryPromise;
this.fieldsToIncrement$
.pipe(
takeUntil(stop$),
@ -148,6 +178,21 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient {
})
)
.subscribe();
this.coreUsageEvents$
.pipe(
takeUntil(stop$),
tap(({ id, isKibanaRequest, types }: CoreUsageEvent) => {
const kibanaYesNo = isKibanaRequest ? 'yes' : 'no';
// NB this usage counter has the domainId: 'core', and so will related docs in 'kibana-usage-counters' data view
types?.forEach((type) =>
incrementUsageCounter({
counterName: `savedObjects.${id}.kibanaRequest.${kibanaYesNo}.types.${type}`,
})
);
})
)
.subscribe();
}
public async getUsageStats() {
@ -251,36 +296,46 @@ export class CoreUsageStatsClient implements ICoreUsageStatsClient {
private async updateUsageStats(
counterFieldNames: string[],
prefix: string,
{ request }: BaseIncrementOptions
id: string,
{ request, types }: BaseIncrementOptions
) {
const fields = this.getFieldsToIncrement(counterFieldNames, prefix, request);
const isKibanaRequest = getIsKibanaRequest(request);
const spaceId = this.getNamespace(request);
const fields = this.getFieldsToIncrement({
counterFieldNames,
prefix: id,
isKibanaRequest,
spaceId,
});
this.coreUsageEvents$.next({ id, isKibanaRequest, types });
this.fieldsToIncrement$.next(fields);
}
private getIsDefaultNamespace(request: KibanaRequest) {
private getNamespace(request: KibanaRequest): string {
const requestBasePath = this.basePath.get(request); // obtain the original request basePath, as it may have been modified by a request interceptor
const pathToCheck = this.basePath.remove(requestBasePath); // remove the server basePath from the request basePath
const matchResult = pathToCheck.match(SPACE_CONTEXT_REGEX); // Look for `/s/space-url-context` in the base path
if (!matchResult || matchResult.length === 0) {
return true;
return DEFAULT_NAMESPACE_STRING;
}
// Ignoring first result, we only want the capture group result at index 1
const [, spaceId] = matchResult;
return spaceId === DEFAULT_NAMESPACE_STRING;
return matchResult[1];
}
private getFieldsToIncrement(
counterFieldNames: string[],
prefix: string,
request: KibanaRequest
) {
const isKibanaRequest = getIsKibanaRequest(request);
const isDefaultNamespace = this.getIsDefaultNamespace(request);
const namespaceField = isDefaultNamespace ? 'default' : 'custom';
private getFieldsToIncrement({
prefix,
counterFieldNames,
spaceId,
isKibanaRequest,
}: {
prefix: string;
counterFieldNames: string[];
spaceId: string;
isKibanaRequest: boolean;
}) {
const namespaceField = spaceId === DEFAULT_NAMESPACE_STRING ? 'default' : 'custom';
return [
'total',
`namespace.${namespaceField}.total`,
@ -302,10 +357,10 @@ function getFieldsForCounter(prefix: string) {
].map((x) => `${prefix}.${x}`);
}
function getIsKibanaRequest({ headers }: KibanaRequest) {
function getIsKibanaRequest({ headers }: KibanaRequest): boolean {
// The presence of these request headers gives us a good indication that this is a first-party request from the Kibana client.
// We can't be 100% certain, but this is a reasonable attempt.
return (
return Boolean(
headers && headers['kbn-version'] && headers.referer && headers['x-elastic-internal-origin']
);
}

View file

@ -99,6 +99,7 @@ describe('POST /api/saved_objects/_bulk_create', () => {
expect(result.body).toEqual(clientResponse);
expect(coreUsageStatsClient.incrementSavedObjectsBulkCreate).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -95,6 +95,7 @@ describe('POST /api/saved_objects/_bulk_delete', () => {
expect(result.body).toEqual(clientResponse);
expect(coreUsageStatsClient.incrementSavedObjectsBulkDelete).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -95,6 +95,7 @@ describe('POST /api/saved_objects/_bulk_get', () => {
expect(result.body).toEqual(clientResponse);
expect(coreUsageStatsClient.incrementSavedObjectsBulkGet).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -99,6 +99,7 @@ describe('POST /api/saved_objects/_bulk_resolve', () => {
expect(result.body).toEqual(clientResponse);
expect(coreUsageStatsClient.incrementSavedObjectsBulkResolve).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -115,6 +115,7 @@ describe('PUT /api/saved_objects/_bulk_update', () => {
expect(result.body).toEqual({ saved_objects: clientResponse });
expect(coreUsageStatsClient.incrementSavedObjectsBulkUpdate).toHaveBeenCalledWith({
request: expect.anything(),
types: ['visualization', 'dashboard'],
});
});

View file

@ -86,6 +86,7 @@ describe('POST /api/saved_objects/{type}', () => {
expect(result.body).toEqual(clientResponse);
expect(coreUsageStatsClient.incrementSavedObjectsCreate).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -72,6 +72,7 @@ describe('DELETE /api/saved_objects/{type}/{id}', () => {
expect(result.body).toEqual({});
expect(coreUsageStatsClient.incrementSavedObjectsDelete).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -158,6 +158,7 @@ describe('GET /api/saved_objects/_find', () => {
expect(result.body).toEqual(findResponse);
expect(coreUsageStatsClient.incrementSavedObjectsFind).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -109,6 +109,7 @@ describe('GET /api/saved_objects/{type}/{id}', () => {
expect(result.body).toEqual(clientResponse);
expect(coreUsageStatsClient.incrementSavedObjectsGet).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -101,6 +101,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
expect(result.body).toEqual(clientResponse);
expect(coreUsageStatsClient.incrementSavedObjectsUpdate).toHaveBeenCalledWith({
request: expect.anything(),
types: ['index-pattern'],
});
});

View file

@ -27,7 +27,7 @@ interface UiCounterEvent {
total: number;
}
export interface UiCountersUsage {
export interface UiUsageCounters {
dailyEvents: UiCounterEvent[];
}
@ -83,7 +83,7 @@ export async function fetchUiCounters({ soClient }: CollectorFetchContext) {
}
export function registerUiCountersUsageCollector(usageCollection: UsageCollectionSetup) {
const collector = usageCollection.makeUsageCollector<UiCountersUsage>({
const collector = usageCollection.makeUsageCollector<UiUsageCounters>({
type: 'ui_counters',
schema: {
dailyEvents: {

View file

@ -24,7 +24,7 @@ interface UsageCounterEvent {
total: number;
}
export interface UiCountersUsage {
export interface UsageCounters {
dailyEvents: UsageCounterEvent[];
}
@ -52,7 +52,7 @@ export function transformRawCounter(
}
export function registerUsageCountersUsageCollector(usageCollection: UsageCollectionSetup) {
const collector = usageCollection.makeUsageCollector<UiCountersUsage>({
const collector = usageCollection.makeUsageCollector<UsageCounters>({
type: 'usage_counters',
schema: {
dailyEvents: {