From 9b962d9b02ce0fa0c4b44e1ff906d7ed4308ee8f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 11 Dec 2020 14:29:38 -0500 Subject: [PATCH] Update core usage stats collection Added fields for namespace (default or custom). --- .../core_usage_data_service.test.ts | 22 +-- .../core_usage_data_service.ts | 7 +- .../core_usage_stats_client.test.ts | 146 ++++++++++++++---- .../core_usage_stats_client.ts | 116 ++++++++------ src/core/server/core_usage_data/types.ts | 24 ++- .../server/saved_objects/routes/export.ts | 3 +- .../server/saved_objects/routes/import.ts | 3 +- .../routes/integration_tests/export.test.ts | 2 +- .../routes/integration_tests/import.test.ts | 2 +- .../resolve_import_errors.test.ts | 2 +- .../routes/resolve_import_errors.ts | 3 +- src/core/server/server.api.md | 36 ++++- src/core/server/server.ts | 1 + .../collectors/core/core_usage_collector.ts | 32 +++- src/plugins/telemetry/schema/oss_plugins.json | 48 +++++- 15 files changed, 326 insertions(+), 121 deletions(-) diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index e22dfcb1e3a20..737c851f03bc9 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -29,6 +29,7 @@ import { config as RawHttpConfig } from '../http/http_config'; import { config as RawLoggingConfig } from '../logging/logging_config'; import { config as RawKibanaConfig } from '../kibana_config'; import { savedObjectsConfig as RawSavedObjectsConfig } from '../saved_objects/saved_objects_config'; +import { httpServiceMock } from '../http/http_service.mock'; import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; @@ -68,11 +69,12 @@ describe('CoreUsageDataService', () => { describe('setup', () => { it('creates internal repository', async () => { + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - service.setup({ metrics, savedObjectsStartPromise }); + service.setup({ http, metrics, savedObjectsStartPromise }); const savedObjects = await savedObjectsStartPromise; expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1); @@ -81,14 +83,12 @@ describe('CoreUsageDataService', () => { describe('#registerType', () => { it('registers core usage stats type', async () => { + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - const coreUsageData = service.setup({ - metrics, - savedObjectsStartPromise, - }); + const coreUsageData = service.setup({ http, metrics, savedObjectsStartPromise }); const typeRegistry = typeRegistryMock.create(); coreUsageData.registerType(typeRegistry); @@ -104,14 +104,12 @@ describe('CoreUsageDataService', () => { describe('#getClient', () => { it('returns client', async () => { + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - const coreUsageData = service.setup({ - metrics, - savedObjectsStartPromise, - }); + const coreUsageData = service.setup({ http, metrics, savedObjectsStartPromise }); const usageStatsClient = coreUsageData.getClient(); expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient); @@ -122,11 +120,12 @@ describe('CoreUsageDataService', () => { describe('start', () => { describe('getCoreUsageData', () => { it('returns core metrics for default config', async () => { + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - service.setup({ metrics, savedObjectsStartPromise }); + service.setup({ http, metrics, savedObjectsStartPromise }); const elasticsearch = elasticsearchServiceMock.createStart(); elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({ body: [ @@ -296,6 +295,7 @@ describe('CoreUsageDataService', () => { observables.push(newObservable); return newObservable; }); + const http = httpServiceMock.createInternalSetupContract(); const metrics = metricsServiceMock.createInternalSetupContract(); metrics.getOpsMetrics$.mockImplementation(() => { const newObservable = hot('-a-------'); @@ -306,7 +306,7 @@ describe('CoreUsageDataService', () => { savedObjectsServiceMock.createStartContract() ); - service.setup({ metrics, savedObjectsStartPromise }); + service.setup({ http, metrics, savedObjectsStartPromise }); // Use the stopTimer$ to delay calling stop() until the third frame const stopTimer$ = cold('---a|'); diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 02b4f2ac59133..07c583186b453 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -24,7 +24,7 @@ import { CoreService } from 'src/core/types'; import { Logger, SavedObjectsServiceStart, SavedObjectTypeRegistry } from 'src/core/server'; import { CoreContext } from '../core_context'; import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config'; -import { HttpConfigType } from '../http'; +import { HttpConfigType, InternalHttpServiceSetup } from '../http'; import { LoggingConfigType } from '../logging'; import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config'; import { @@ -42,6 +42,7 @@ import { CoreUsageStatsClient } from './core_usage_stats_client'; import { MetricsServiceSetup, OpsMetrics } from '..'; export interface SetupDeps { + http: InternalHttpServiceSetup; metrics: MetricsServiceSetup; savedObjectsStartPromise: Promise; } @@ -248,7 +249,7 @@ export class CoreUsageDataService implements CoreService { const debugLogger = (message: string) => this.logger.debug(message); - return new CoreUsageStatsClient(debugLogger, internalRepositoryPromise); + return new CoreUsageStatsClient(debugLogger, http.basePath, internalRepositoryPromise); }; this.coreUsageStatsClient = getClient(); diff --git a/src/core/server/core_usage_data/core_usage_stats_client.test.ts b/src/core/server/core_usage_data/core_usage_stats_client.test.ts index e4f47667fce6b..e2b79e7ae8a5e 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.test.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { savedObjectsRepositoryMock } from '../mocks'; +import { httpServerMock, httpServiceMock, savedObjectsRepositoryMock } from '../mocks'; import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants'; import { IncrementSavedObjectsImportOptions, @@ -28,18 +28,22 @@ import { EXPORT_STATS_PREFIX, } from './core_usage_stats_client'; import { CoreUsageStatsClient } from '.'; +import { DEFAULT_NAMESPACE_STRING } from '../saved_objects/service/lib/utils'; describe('CoreUsageStatsClient', () => { - const setup = () => { + 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) ); - return { usageStatsClient, debugLoggerMock, repositoryMock }; + return { usageStatsClient, debugLoggerMock, basePathMock, repositoryMock }; }; - const firstPartyRequestHeaders = { 'kbn-version': 'a', origin: 'b', referer: 'c' }; // as long as these three header fields are truthy, this will be treated like a first-party request const incrementOptions = { refresh: false }; @@ -72,8 +76,11 @@ describe('CoreUsageStatsClient', () => { const { usageStatsClient, repositoryMock } = setup(); repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + const request = httpServerMock.createKibanaRequest(); await expect( - usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions) + usageStatsClient.incrementSavedObjectsImport({ + request, + } as IncrementSavedObjectsImportOptions) ).resolves.toBeUndefined(); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); }); @@ -81,14 +88,18 @@ describe('CoreUsageStatsClient', () => { it('handles falsy options appropriately', async () => { const { usageStatsClient, repositoryMock } = setup(); - await usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions); + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsImport({ + request, + } as IncrementSavedObjectsImportOptions); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.kibanaRequest.no`, + `${IMPORT_STATS_PREFIX}.namespace.default.total`, + `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, ], @@ -96,11 +107,12 @@ describe('CoreUsageStatsClient', () => { ); }); - it('handles truthy options appropriately', async () => { - const { usageStatsClient, repositoryMock } = setup(); + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); await usageStatsClient.incrementSavedObjectsImport({ - headers: firstPartyRequestHeaders, + request, createNewCopies: true, overwrite: true, } as IncrementSavedObjectsImportOptions); @@ -110,13 +122,36 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_ID, [ `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${IMPORT_STATS_PREFIX}.namespace.default.total`, + `${IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, ], incrementOptions ); }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsImport({ + request, + } as IncrementSavedObjectsImportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${IMPORT_STATS_PREFIX}.total`, + `${IMPORT_STATS_PREFIX}.namespace.custom.total`, + `${IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, + `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, + ], + incrementOptions + ); + }); }); describe('#incrementSavedObjectsResolveImportErrors', () => { @@ -124,10 +159,11 @@ describe('CoreUsageStatsClient', () => { const { usageStatsClient, repositoryMock } = setup(); repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + const request = httpServerMock.createKibanaRequest(); await expect( - usageStatsClient.incrementSavedObjectsResolveImportErrors( - {} as IncrementSavedObjectsResolveImportErrorsOptions - ) + usageStatsClient.incrementSavedObjectsResolveImportErrors({ + request, + } as IncrementSavedObjectsResolveImportErrorsOptions) ).resolves.toBeUndefined(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -135,27 +171,30 @@ describe('CoreUsageStatsClient', () => { it('handles falsy options appropriately', async () => { const { usageStatsClient, repositoryMock } = setup(); - await usageStatsClient.incrementSavedObjectsResolveImportErrors( - {} as IncrementSavedObjectsResolveImportErrorsOptions - ); + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsResolveImportErrors({ + request, + } as IncrementSavedObjectsResolveImportErrorsOptions); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, [ `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, ], incrementOptions ); }); - it('handles truthy options appropriately', async () => { - const { usageStatsClient, repositoryMock } = setup(); + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); await usageStatsClient.incrementSavedObjectsResolveImportErrors({ - headers: firstPartyRequestHeaders, + request, createNewCopies: true, } as IncrementSavedObjectsResolveImportErrorsOptions); expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); @@ -164,12 +203,34 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_ID, [ `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, ], incrementOptions ); }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsResolveImportErrors({ + request, + } as IncrementSavedObjectsResolveImportErrorsOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${RESOLVE_IMPORT_STATS_PREFIX}.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.total`, + `${RESOLVE_IMPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, + ], + incrementOptions + ); + }); }); describe('#incrementSavedObjectsExport', () => { @@ -177,8 +238,11 @@ describe('CoreUsageStatsClient', () => { const { usageStatsClient, repositoryMock } = setup(); repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!')); + const request = httpServerMock.createKibanaRequest(); await expect( - usageStatsClient.incrementSavedObjectsExport({} as IncrementSavedObjectsExportOptions) + usageStatsClient.incrementSavedObjectsExport({ + request, + } as IncrementSavedObjectsExportOptions) ).resolves.toBeUndefined(); expect(repositoryMock.incrementCounter).toHaveBeenCalled(); }); @@ -186,7 +250,9 @@ describe('CoreUsageStatsClient', () => { it('handles falsy options appropriately', async () => { const { usageStatsClient, repositoryMock } = setup(); + const request = httpServerMock.createKibanaRequest(); await usageStatsClient.incrementSavedObjectsExport({ + request, types: undefined, supportedTypes: ['foo', 'bar'], } as IncrementSavedObjectsExportOptions); @@ -196,18 +262,20 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_ID, [ `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.kibanaRequest.no`, + `${EXPORT_STATS_PREFIX}.namespace.default.total`, + `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.no`, `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, ], incrementOptions ); }); - it('handles truthy options appropriately', async () => { - const { usageStatsClient, repositoryMock } = setup(); + it('handles truthy options and the default namespace string appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup(DEFAULT_NAMESPACE_STRING); + const request = httpServerMock.createKibanaRequest({ headers: firstPartyRequestHeaders }); await usageStatsClient.incrementSavedObjectsExport({ - headers: firstPartyRequestHeaders, + request, types: ['foo', 'bar'], supportedTypes: ['foo', 'bar'], } as IncrementSavedObjectsExportOptions); @@ -217,11 +285,33 @@ describe('CoreUsageStatsClient', () => { CORE_USAGE_STATS_ID, [ `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.kibanaRequest.yes`, + `${EXPORT_STATS_PREFIX}.namespace.default.total`, + `${EXPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`, `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, ], incrementOptions ); }); + + it('handles a non-default space appropriately', async () => { + const { usageStatsClient, repositoryMock } = setup('foo'); + + const request = httpServerMock.createKibanaRequest(); + await usageStatsClient.incrementSavedObjectsExport({ + request, + } as IncrementSavedObjectsExportOptions); + expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1); + expect(repositoryMock.incrementCounter).toHaveBeenCalledWith( + CORE_USAGE_STATS_TYPE, + CORE_USAGE_STATS_ID, + [ + `${EXPORT_STATS_PREFIX}.total`, + `${EXPORT_STATS_PREFIX}.namespace.custom.total`, + `${EXPORT_STATS_PREFIX}.namespace.custom.kibanaRequest.no`, + `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, + ], + incrementOptions + ); + }); }); }); diff --git a/src/core/server/core_usage_data/core_usage_stats_client.ts b/src/core/server/core_usage_data/core_usage_stats_client.ts index 58356832d8b8a..7e4d12b6a462c 100644 --- a/src/core/server/core_usage_data/core_usage_stats_client.ts +++ b/src/core/server/core_usage_data/core_usage_stats_client.ts @@ -19,16 +19,18 @@ import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants'; import { CoreUsageStats } from './types'; +import { DEFAULT_NAMESPACE_STRING } from '../saved_objects/service/lib/utils'; import { - Headers, ISavedObjectsRepository, SavedObjectsImportOptions, SavedObjectsResolveImportErrorsOptions, SavedObjectsExportOptions, + KibanaRequest, + IBasePath, } from '..'; interface BaseIncrementOptions { - headers?: Headers; + request: KibanaRequest; } /** @internal */ export type IncrementSavedObjectsImportOptions = BaseIncrementOptions & @@ -44,29 +46,26 @@ export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport'; export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors'; export const EXPORT_STATS_PREFIX = 'apiCalls.savedObjectsExport'; const ALL_COUNTER_FIELDS = [ - `${IMPORT_STATS_PREFIX}.total`, - `${IMPORT_STATS_PREFIX}.kibanaRequest.yes`, - `${IMPORT_STATS_PREFIX}.kibanaRequest.no`, + // Saved Objects Management APIs + ...getAllCommonFields(IMPORT_STATS_PREFIX), `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, `${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`, `${IMPORT_STATS_PREFIX}.overwriteEnabled.no`, - `${RESOLVE_IMPORT_STATS_PREFIX}.total`, - `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`, - `${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`, + ...getAllCommonFields(RESOLVE_IMPORT_STATS_PREFIX), `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`, `${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`, - `${EXPORT_STATS_PREFIX}.total`, - `${EXPORT_STATS_PREFIX}.kibanaRequest.yes`, - `${EXPORT_STATS_PREFIX}.kibanaRequest.no`, + ...getAllCommonFields(EXPORT_STATS_PREFIX), `${EXPORT_STATS_PREFIX}.allTypesSelected.yes`, `${EXPORT_STATS_PREFIX}.allTypesSelected.no`, ]; +const SPACE_CONTEXT_REGEX = /^\/s\/([a-z0-9_\-]+)/; /** @internal */ export class CoreUsageStatsClient { constructor( private readonly debugLogger: (message: string) => void, + private readonly basePath: IBasePath, private readonly repositoryPromise: Promise ) {} @@ -88,66 +87,91 @@ export class CoreUsageStatsClient { return coreUsageStats; } - public async incrementSavedObjectsImport({ - headers, - createNewCopies, - overwrite, - }: IncrementSavedObjectsImportOptions) { - const isKibanaRequest = getIsKibanaRequest(headers); + public async incrementSavedObjectsImport(options: IncrementSavedObjectsImportOptions) { + const { createNewCopies, overwrite } = options; const counterFieldNames = [ - 'total', - `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, `overwriteEnabled.${overwrite ? 'yes' : 'no'}`, ]; - await this.updateUsageStats(counterFieldNames, IMPORT_STATS_PREFIX); + await this.updateUsageStats(counterFieldNames, IMPORT_STATS_PREFIX, options); } - public async incrementSavedObjectsResolveImportErrors({ - headers, - createNewCopies, - }: IncrementSavedObjectsResolveImportErrorsOptions) { - const isKibanaRequest = getIsKibanaRequest(headers); - const counterFieldNames = [ - 'total', - `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, - `createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`, - ]; - await this.updateUsageStats(counterFieldNames, RESOLVE_IMPORT_STATS_PREFIX); + public async incrementSavedObjectsResolveImportErrors( + options: IncrementSavedObjectsResolveImportErrorsOptions + ) { + const { createNewCopies } = options; + const counterFieldNames = [`createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`]; + await this.updateUsageStats(counterFieldNames, RESOLVE_IMPORT_STATS_PREFIX, options); } - public async incrementSavedObjectsExport({ - headers, - types, - supportedTypes, - }: IncrementSavedObjectsExportOptions) { - const isKibanaRequest = getIsKibanaRequest(headers); + public async incrementSavedObjectsExport(options: IncrementSavedObjectsExportOptions) { + const { types, supportedTypes } = options; const isAllTypesSelected = !!types && supportedTypes.every((x) => types.includes(x)); - const counterFieldNames = [ - 'total', - `kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, - `allTypesSelected.${isAllTypesSelected ? 'yes' : 'no'}`, - ]; - await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX); + const counterFieldNames = [`allTypesSelected.${isAllTypesSelected ? 'yes' : 'no'}`]; + await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX, options); } - private async updateUsageStats(counterFieldNames: string[], prefix: string) { + private async updateUsageStats( + counterFieldNames: string[], + prefix: string, + { request }: BaseIncrementOptions + ) { const options = { refresh: false }; try { const repository = await this.repositoryPromise; + const fields = [...this.getCommonFieldsToIncrement(request), ...counterFieldNames]; await repository.incrementCounter( CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID, - counterFieldNames.map((x) => `${prefix}.${x}`), + fields.map((x) => `${prefix}.${x}`), options ); } catch (err) { // do nothing } } + + private getIsDefaultNamespace(request: KibanaRequest) { + 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; + } + + // Ignoring first result, we only want the capture group result at index 1 + const [, spaceId] = matchResult; + + return spaceId === DEFAULT_NAMESPACE_STRING; + } + + private getCommonFieldsToIncrement(request: KibanaRequest) { + const isKibanaRequest = getIsKibanaRequest(request); + const isDefaultNamespace = this.getIsDefaultNamespace(request); + const namespaceField = isDefaultNamespace ? 'default' : 'custom'; + const counterFieldNames = [ + 'total', + `namespace.${namespaceField}.total`, + `namespace.${namespaceField}.kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`, + ]; + return counterFieldNames; + } +} + +function getAllCommonFields(prefix: string) { + return [ + 'total', + 'namespace.default.total', + 'namespace.default.kibanaRequest.yes', + 'namespace.default.kibanaRequest.no', + 'namespace.custom.total', + 'namespace.custom.kibanaRequest.yes', + 'namespace.custom.kibanaRequest.no', + ].map((x) => `${prefix}.${x}`); } -function getIsKibanaRequest(headers?: Headers) { +function getIsKibanaRequest({ headers }: KibanaRequest) { // The presence of these three 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 headers && headers['kbn-version'] && headers.origin && headers.referer; diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index aa41d75e6f2d4..2976da8c1a9a7 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -28,20 +28,32 @@ import { ISavedObjectTypeRegistry, SavedObjectTypeRegistry } from '..'; * */ export interface CoreUsageStats { 'apiCalls.savedObjectsImport.total'?: number; - 'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number; - 'apiCalls.savedObjectsImport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsImport.namespace.default.total'?: number; + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsImport.namespace.custom.total'?: number; + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no'?: number; 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number; 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number; 'apiCalls.savedObjectsImport.overwriteEnabled.yes'?: number; 'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number; 'apiCalls.savedObjectsResolveImportErrors.total'?: number; - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number; - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.total'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.total'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.no'?: number; 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number; 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number; 'apiCalls.savedObjectsExport.total'?: number; - 'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number; - 'apiCalls.savedObjectsExport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsExport.namespace.default.total'?: number; + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.total'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no'?: number; 'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number; 'apiCalls.savedObjectsExport.allTypesSelected.no'?: number; } diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 387280d777eaa..8f5c19d927d40 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -104,10 +104,9 @@ export const registerExportRoute = ( } } - const { headers } = req; const usageStatsClient = coreUsageData.getClient(); usageStatsClient - .incrementSavedObjectsExport({ headers, types, supportedTypes }) + .incrementSavedObjectsExport({ request: req, types, supportedTypes }) .catch(() => {}); const exportStream = await exportSavedObjectsToStream({ diff --git a/src/core/server/saved_objects/routes/import.ts b/src/core/server/saved_objects/routes/import.ts index 27be710c0a92a..ebc52c32e2c70 100644 --- a/src/core/server/saved_objects/routes/import.ts +++ b/src/core/server/saved_objects/routes/import.ts @@ -75,10 +75,9 @@ export const registerImportRoute = ( router.handleLegacyErrors(async (context, req, res) => { const { overwrite, createNewCopies } = req.query; - const { headers } = req; const usageStatsClient = coreUsageData.getClient(); usageStatsClient - .incrementSavedObjectsImport({ headers, createNewCopies, overwrite }) + .incrementSavedObjectsImport({ request: req, createNewCopies, overwrite }) .catch(() => {}); const file = req.body.file as FileStream; diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index c37ed2da97681..1d4c5b7cbd15d 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -118,7 +118,7 @@ describe('POST /api/saved_objects/_export', () => { }) ); expect(coreUsageStatsClient.incrementSavedObjectsExport).toHaveBeenCalledWith({ - headers: expect.anything(), + request: expect.anything(), types: ['search'], supportedTypes: ['index-pattern', 'search'], }); diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 9dfb7f79a925d..33885eb72b718 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -106,7 +106,7 @@ describe(`POST ${URL}`, () => { expect(result.body).toEqual({ success: true, successCount: 0 }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created expect(coreUsageStatsClient.incrementSavedObjectsImport).toHaveBeenCalledWith({ - headers: expect.anything(), + request: expect.anything(), createNewCopies: false, overwrite: false, }); diff --git a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts index 46f4d2435bf67..64762e341185d 100644 --- a/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/resolve_import_errors.test.ts @@ -117,7 +117,7 @@ describe(`POST ${URL}`, () => { expect(result.body).toEqual({ success: true, successCount: 0 }); expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created expect(coreUsageStatsClient.incrementSavedObjectsResolveImportErrors).toHaveBeenCalledWith({ - headers: expect.anything(), + request: expect.anything(), createNewCopies: false, }); }); diff --git a/src/core/server/saved_objects/routes/resolve_import_errors.ts b/src/core/server/saved_objects/routes/resolve_import_errors.ts index 34c178a975304..5db5454b224d7 100644 --- a/src/core/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/core/server/saved_objects/routes/resolve_import_errors.ts @@ -83,10 +83,9 @@ export const registerResolveImportErrorsRoute = ( router.handleLegacyErrors(async (context, req, res) => { const { createNewCopies } = req.query; - const { headers } = req; const usageStatsClient = coreUsageData.getClient(); usageStatsClient - .incrementSavedObjectsResolveImportErrors({ headers, createNewCopies }) + .incrementSavedObjectsResolveImportErrors({ request: req, createNewCopies }) .catch(() => {}); const file = req.body.file as FileStream; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a39bbecd16ff5..5557a09197fa9 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -546,9 +546,17 @@ export interface CoreUsageStats { // (undocumented) 'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number; // (undocumented) - 'apiCalls.savedObjectsExport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no'?: number; // (undocumented) - 'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsExport.namespace.default.total'?: number; // (undocumented) 'apiCalls.savedObjectsExport.total'?: number; // (undocumented) @@ -556,9 +564,17 @@ export interface CoreUsageStats { // (undocumented) 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number; // (undocumented) - 'apiCalls.savedObjectsImport.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.namespace.custom.total'?: number; // (undocumented) - 'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsImport.namespace.default.total'?: number; // (undocumented) 'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number; // (undocumented) @@ -570,9 +586,17 @@ export interface CoreUsageStats { // (undocumented) 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number; // (undocumented) - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.yes'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.total'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.no'?: number; + // (undocumented) + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.yes'?: number; // (undocumented) - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number; + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.total'?: number; // (undocumented) 'apiCalls.savedObjectsResolveImportErrors.total'?: number; } diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 75530e557de04..08f0a191151dd 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -166,6 +166,7 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); const coreUsageDataSetup = this.coreUsageData.setup({ + http: httpSetup, metrics: metricsSetup, savedObjectsStartPromise: this.savedObjectsStartPromise, }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index d30a3c5ab6861..eaad760c922ba 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -116,20 +116,40 @@ export function getCoreUsageCollector( }, }, 'apiCalls.savedObjectsImport.total': { type: 'long' }, - 'apiCalls.savedObjectsImport.kibanaRequest.yes': { type: 'long' }, - 'apiCalls.savedObjectsImport.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no': { type: 'long' }, 'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes': { type: 'long' }, 'apiCalls.savedObjectsImport.createNewCopiesEnabled.no': { type: 'long' }, 'apiCalls.savedObjectsImport.overwriteEnabled.yes': { type: 'long' }, 'apiCalls.savedObjectsImport.overwriteEnabled.no': { type: 'long' }, 'apiCalls.savedObjectsResolveImportErrors.total': { type: 'long' }, - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes': { type: 'long' }, - 'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.yes': { + type: 'long', + }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.no': { + type: 'long', + }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.yes': { + type: 'long', + }, + 'apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.no': { + type: 'long', + }, 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes': { type: 'long' }, 'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no': { type: 'long' }, 'apiCalls.savedObjectsExport.total': { type: 'long' }, - 'apiCalls.savedObjectsExport.kibanaRequest.yes': { type: 'long' }, - 'apiCalls.savedObjectsExport.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.default.total': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.default.kibanaRequest.no': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.custom.total': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.yes': { type: 'long' }, + 'apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no': { type: 'long' }, 'apiCalls.savedObjectsExport.allTypesSelected.yes': { type: 'long' }, 'apiCalls.savedObjectsExport.allTypesSelected.no': { type: 'long' }, }, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 55384329f9af7..b14a5907c7ab4 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1520,10 +1520,22 @@ "apiCalls.savedObjectsImport.total": { "type": "long" }, - "apiCalls.savedObjectsImport.kibanaRequest.yes": { + "apiCalls.savedObjectsImport.namespace.default.total": { "type": "long" }, - "apiCalls.savedObjectsImport.kibanaRequest.no": { + "apiCalls.savedObjectsImport.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsImport.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsImport.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsImport.namespace.custom.kibanaRequest.no": { "type": "long" }, "apiCalls.savedObjectsImport.createNewCopiesEnabled.yes": { @@ -1541,10 +1553,22 @@ "apiCalls.savedObjectsResolveImportErrors.total": { "type": "long" }, - "apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes": { + "apiCalls.savedObjectsResolveImportErrors.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.namespace.default.kibanaRequest.no": { "type": "long" }, - "apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no": { + "apiCalls.savedObjectsResolveImportErrors.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsResolveImportErrors.namespace.custom.kibanaRequest.no": { "type": "long" }, "apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes": { @@ -1556,10 +1580,22 @@ "apiCalls.savedObjectsExport.total": { "type": "long" }, - "apiCalls.savedObjectsExport.kibanaRequest.yes": { + "apiCalls.savedObjectsExport.namespace.default.total": { + "type": "long" + }, + "apiCalls.savedObjectsExport.namespace.default.kibanaRequest.yes": { + "type": "long" + }, + "apiCalls.savedObjectsExport.namespace.default.kibanaRequest.no": { + "type": "long" + }, + "apiCalls.savedObjectsExport.namespace.custom.total": { + "type": "long" + }, + "apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.yes": { "type": "long" }, - "apiCalls.savedObjectsExport.kibanaRequest.no": { + "apiCalls.savedObjectsExport.namespace.custom.kibanaRequest.no": { "type": "long" }, "apiCalls.savedObjectsExport.allTypesSelected.yes": {