mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
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:
parent
7bb7f21426
commit
9d01ab1ee8
33 changed files with 516 additions and 172 deletions
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}"`,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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']
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue