diff --git a/x-pack/plugins/ml/mappings.json b/x-pack/plugins/ml/mappings.json deleted file mode 100644 index 041b85dbea4a1e..00000000000000 --- a/x-pack/plugins/ml/mappings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type" : "long" - } - } - } - } - } -} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 624e877bda49fe..79aebece85af2d 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { SharePluginStart } from 'src/plugins/share/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { SecurityPluginSetup } from '../../security/public'; @@ -24,6 +25,7 @@ export interface MlSetupDependencies { security: SecurityPluginSetup; licensing: LicensingPluginSetup; management: ManagementSetup; + usageCollection: UsageCollectionSetup; } export class MlPlugin implements Plugin { @@ -47,6 +49,7 @@ export class MlPlugin implements Plugin { security: pluginsSetup.security, licensing: pluginsSetup.licensing, management: pluginsSetup.management, + usageCollection: pluginsSetup.usageCollection, }, { element: params.element, diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/index.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/index.ts deleted file mode 100644 index dffd95f50e0d95..00000000000000 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { - createMlTelemetry, - incrementFileDataVisualizerIndexCreationCount, - storeMlTelemetry, - MlTelemetry, - MlTelemetrySavedObject, - ML_TELEMETRY_DOC_ID, -} from './ml_telemetry'; -export { makeMlUsageCollector } from './make_ml_usage_collector'; diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts deleted file mode 100644 index 15a430a08eac19..00000000000000 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { SavedObjectsServiceStart } from 'kibana/server'; -import { - createMlTelemetry, - ML_TELEMETRY_DOC_ID, - MlTelemetry, - MlTelemetrySavedObject, -} from './ml_telemetry'; - -export function makeMlUsageCollector( - usageCollection: UsageCollectionSetup | undefined, - savedObjects: SavedObjectsServiceStart -): void { - if (!usageCollection) { - return; - } - - const mlUsageCollector = usageCollection.makeUsageCollector({ - type: 'ml', - isReady: () => true, - fetch: async (): Promise => { - try { - const mlTelemetrySavedObject: MlTelemetrySavedObject = await savedObjects - .createInternalRepository() - .get('ml-telemetry', ML_TELEMETRY_DOC_ID); - - return mlTelemetrySavedObject.attributes; - } catch (err) { - return createMlTelemetry(); - } - }, - }); - - usageCollection.registerCollector(mlUsageCollector); -} diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts deleted file mode 100644 index cda160877f7aef..00000000000000 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// import { -// createMlTelemetry, -// incrementFileDataVisualizerIndexCreationCount, -// ML_TELEMETRY_DOC_ID, -// MlTelemetry, -// storeMlTelemetry, -// } from './ml_telemetry'; - -describe('ml_telemetry', () => { - describe('createMlTelemetry', () => { - it('should create a MlTelemetry object', () => { - // const mlTelemetry = createMlTelemetry(1); - // expect(mlTelemetry.file_data_visualizer.index_creation_count).toBe(1); - }); - it('should ignore undefined or unknown values', () => { - // const mlTelemetry = createMlTelemetry(undefined); - // expect(mlTelemetry.file_data_visualizer.index_creation_count).toBe(0); - }); - }); - - describe('storeMlTelemetry', () => { - // let mlTelemetry: MlTelemetry; - // let internalRepository: any; - - // beforeEach(() => { - // internalRepository = { create: jest.fn(), get: jest.fn() }; - // mlTelemetry = { - // file_data_visualizer: { - // index_creation_count: 1, - // }, - // }; - // }); - - it('should call internalRepository create with the given MlTelemetry object', () => { - // storeMlTelemetry(internalRepository, mlTelemetry); - // expect(internalRepository.create.mock.calls[0][1]).toBe(mlTelemetry); - }); - - it('should call internalRepository create with the ml-telemetry document type and ID', () => { - // storeMlTelemetry(internalRepository, mlTelemetry); - // expect(internalRepository.create.mock.calls[0][0]).toBe('ml-telemetry'); - // expect(internalRepository.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID); - }); - - it('should call internalRepository create with overwrite: true', () => { - // storeMlTelemetry(internalRepository, mlTelemetry); - // expect(internalRepository.create.mock.calls[0][2].overwrite).toBe(true); - }); - }); - - describe('incrementFileDataVisualizerIndexCreationCount', () => { - // let savedObjectsClient: any; - - // function createSavedObjectsClientInstance( - // telemetryEnabled?: boolean, - // indexCreationCount?: number - // ) { - // return { - // create: jest.fn(), - // get: jest.fn(obj => { - // switch (obj) { - // case 'telemetry': - // if (telemetryEnabled === undefined) { - // throw Error; - // } - // return { - // attributes: { - // enabled: telemetryEnabled, - // }, - // }; - // case 'ml-telemetry': - // // emulate that a non-existing saved object will throw an error - // if (indexCreationCount === undefined) { - // throw Error; - // } - // return { - // attributes: { - // file_data_visualizer: { - // index_creation_count: indexCreationCount, - // }, - // }, - // }; - // } - // }), - // }; - // } - - // function mockInit(telemetryEnabled?: boolean, indexCreationCount?: number): void { - // savedObjectsClient = createSavedObjectsClientInstance(telemetryEnabled, indexCreationCount); - // } - - it('should not increment if telemetry status cannot be determined', async () => { - // mockInit(); - // await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - // expect(savedObjectsClient.create.mock.calls).toHaveLength(0); - }); - - it('should not increment if telemetry status is disabled', async () => { - // mockInit(false); - // await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - // expect(savedObjectsClient.create.mock.calls).toHaveLength(0); - }); - - it('should initialize index_creation_count with 1', async () => { - // mockInit(true); - // await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - // expect(savedObjectsClient.create.mock.calls[0][0]).toBe('ml-telemetry'); - // expect(savedObjectsClient.create.mock.calls[0][1]).toEqual({ - // file_data_visualizer: { index_creation_count: 1 }, - // }); - }); - - it('should increment index_creation_count to 2', async () => { - // mockInit(true, 1); - // await incrementFileDataVisualizerIndexCreationCount(savedObjectsClient); - // expect(savedObjectsClient.create.mock.calls[0][0]).toBe('ml-telemetry'); - // expect(savedObjectsClient.create.mock.calls[0][1]).toEqual({ - // file_data_visualizer: { index_creation_count: 2 }, - // }); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts deleted file mode 100644 index 1ca155582db11d..00000000000000 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObjectAttributes, SavedObjectsClientContract } from 'kibana/server'; - -export interface MlTelemetry extends SavedObjectAttributes { - file_data_visualizer: { - index_creation_count: number; - }; -} - -export interface MlTelemetrySavedObject { - attributes: MlTelemetry; -} - -export const ML_TELEMETRY_DOC_ID = 'ml-telemetry'; - -export function createMlTelemetry(count: number = 0): MlTelemetry { - return { - file_data_visualizer: { - index_creation_count: count, - }, - }; -} -// savedObjects -export function storeMlTelemetry( - savedObjectsClient: SavedObjectsClientContract, - mlTelemetry: MlTelemetry -): void { - savedObjectsClient.create('ml-telemetry', mlTelemetry, { - id: ML_TELEMETRY_DOC_ID, - overwrite: true, - }); -} - -export async function incrementFileDataVisualizerIndexCreationCount( - savedObjectsClient: SavedObjectsClientContract -): Promise { - return; - try { - const { attributes } = await savedObjectsClient.get<{ enabled: boolean }>( - 'telemetry', - 'telemetry' - ); - - if (attributes.enabled === false) { - return; - } - } catch (error) { - // if we aren't allowed to get the telemetry document, - // we assume we couldn't opt in to telemetry and won't increment the index count. - return; - } - - let indicesCount = 1; - - try { - const { attributes } = (await savedObjectsClient.get( - 'ml-telemetry', - ML_TELEMETRY_DOC_ID - )) as MlTelemetrySavedObject; - indicesCount = attributes.file_data_visualizer.index_creation_count + 1; - } catch (e) { - /* silently fail, this will happen if the saved object doesn't exist yet. */ - } - - const mlTelemetry = createMlTelemetry(indicesCount); - storeMlTelemetry(savedObjectsClient, mlTelemetry); -} diff --git a/x-pack/plugins/ml/server/lib/telemetry/index.ts b/x-pack/plugins/ml/server/lib/telemetry/index.ts new file mode 100644 index 00000000000000..b5ec80daf17878 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { initMlTelemetry } from './ml_usage_collector'; +export { updateTelemetry } from './telemetry'; diff --git a/x-pack/plugins/ml/server/lib/telemetry/internal_repository.ts b/x-pack/plugins/ml/server/lib/telemetry/internal_repository.ts new file mode 100644 index 00000000000000..a273ea4baadfa0 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/internal_repository.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsServiceStart, ISavedObjectsRepository } from 'kibana/server'; + +let internalRepository: ISavedObjectsRepository | null = null; +export const setInternalRepository = ( + createInternalRepository: SavedObjectsServiceStart['createInternalRepository'] +) => { + internalRepository = createInternalRepository(); +}; +export const getInternalRepository = () => internalRepository; diff --git a/x-pack/plugins/ml/server/lib/telemetry/mappings.ts b/x-pack/plugins/ml/server/lib/telemetry/mappings.ts new file mode 100644 index 00000000000000..87e2243328422c --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/mappings.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsType } from 'src/core/server'; +import { TELEMETRY_DOC_ID } from './telemetry'; + +export const mlTelemetryMappingsType: SavedObjectsType = { + name: TELEMETRY_DOC_ID, + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + file_data_visualizer: { + properties: { + index_creation_count: { + type: 'long', + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts new file mode 100644 index 00000000000000..21e5dce8e47067 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'kibana/server'; + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { getTelemetry, initTelemetry } from './telemetry'; +import { mlTelemetryMappingsType } from './mappings'; +import { setInternalRepository } from './internal_repository'; + +const TELEMETRY_TYPE = 'mlTelemetry'; + +export function initMlTelemetry(coreSetup: CoreSetup, usageCollection: UsageCollectionSetup) { + coreSetup.savedObjects.registerType(mlTelemetryMappingsType); + registerMlUsageCollector(usageCollection); + coreSetup.getStartServices().then(([core]) => { + setInternalRepository(core.savedObjects.createInternalRepository); + }); +} + +function registerMlUsageCollector(usageCollection: UsageCollectionSetup): void { + const mlUsageCollector = usageCollection.makeUsageCollector({ + type: TELEMETRY_TYPE, + isReady: () => true, + fetch: async () => (await getTelemetry()) || initTelemetry(), + }); + + usageCollection.registerCollector(mlUsageCollector); +} diff --git a/x-pack/plugins/ml/server/lib/telemetry/telemetry.test.ts b/x-pack/plugins/ml/server/lib/telemetry/telemetry.test.ts new file mode 100644 index 00000000000000..f41c4fda93a54f --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/telemetry.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getTelemetry, updateTelemetry } from './telemetry'; + +const internalRepository = () => ({ + get: jest.fn(() => null), + create: jest.fn(() => ({ attributes: 'test' })), + update: jest.fn(() => ({ attributes: 'test' })), +}); + +function mockInit(getVal: any = { attributes: {} }): any { + return { + ...internalRepository(), + get: jest.fn(() => getVal), + }; +} + +describe('ml plugin telemetry', () => { + describe('getTelemetry', () => { + it('should get existing telemetry', async () => { + const internalRepo = mockInit(); + await getTelemetry(internalRepo); + expect(internalRepo.update.mock.calls.length).toBe(0); + expect(internalRepo.get.mock.calls.length).toBe(1); + expect(internalRepo.create.mock.calls.length).toBe(0); + }); + }); + + describe('updateTelemetry', () => { + it('should update existing telemetry', async () => { + const internalRepo = mockInit({ + attributes: { + file_data_visualizer: { + index_creation_count: 2, + }, + }, + }); + + await updateTelemetry(internalRepo); + expect(internalRepo.update.mock.calls.length).toBe(1); + expect(internalRepo.get.mock.calls.length).toBe(1); + expect(internalRepo.create.mock.calls.length).toBe(0); + }); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts b/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts new file mode 100644 index 00000000000000..bc56e8b2a43722 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { ISavedObjectsRepository } from 'kibana/server'; + +import { getInternalRepository } from './internal_repository'; + +export const TELEMETRY_DOC_ID = 'ml-telemetry'; + +interface Telemetry { + file_data_visualizer: { + index_creation_count: number; + }; +} + +export interface TelemetrySavedObject { + attributes: Telemetry; +} + +export function initTelemetry(): Telemetry { + return { + file_data_visualizer: { + index_creation_count: 0, + }, + }; +} + +export async function getTelemetry( + internalRepository?: ISavedObjectsRepository +): Promise { + if (internalRepository === undefined) { + return null; + } + + let telemetrySavedObject; + + try { + telemetrySavedObject = await internalRepository.get( + TELEMETRY_DOC_ID, + TELEMETRY_DOC_ID + ); + } catch (e) { + // Fail silently + } + + return telemetrySavedObject ? telemetrySavedObject.attributes : null; +} + +export async function updateTelemetry(internalRepo?: ISavedObjectsRepository) { + const internalRepository = internalRepo || getInternalRepository(); + if (internalRepository === null) { + return; + } + + let telemetry = await getTelemetry(internalRepository); + // Create if doesn't exist + if (telemetry === null || _.isEmpty(telemetry)) { + const newTelemetrySavedObject = await internalRepository.create( + TELEMETRY_DOC_ID, + initTelemetry(), + { id: TELEMETRY_DOC_ID } + ); + telemetry = newTelemetrySavedObject.attributes; + } + + if (telemetry !== null) { + await internalRepository.update(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID, incrementCounts(telemetry)); + } +} + +function incrementCounts(telemetry: Telemetry) { + return { + file_data_visualizer: { + index_creation_count: telemetry.file_data_visualizer.index_creation_count + 1, + }, + }; +} diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 01d0bcc8670194..8948d232b9e5e1 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -16,7 +16,7 @@ import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; -import { makeMlUsageCollector } from './lib/ml_telemetry'; +import { initMlTelemetry } from './lib/telemetry'; import { initMlServerLog } from './client/log'; import { initSampleDataSets } from './lib/sample_data_sets'; @@ -130,9 +130,7 @@ export class MlServerPlugin implements Plugin { - makeMlUsageCollector(plugins.usageCollection, core.savedObjects); - }); + initMlTelemetry(coreSetup, plugins.usageCollection); return createSharedServices(this.mlLicense, plugins.spaces, plugins.cloud); } diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index a14d51ae61b05f..fcfd6e121c9f16 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -19,7 +19,7 @@ import { } from '../models/file_data_visualizer'; import { RouteInitialization } from '../types'; -import { incrementFileDataVisualizerIndexCreationCount } from '../lib/ml_telemetry'; +import { updateTelemetry } from '../lib/telemetry'; function analyzeFiles(context: RequestHandlerContext, data: InputData, overrides: InputOverrides) { const { analyzeFile } = fileDataVisualizerProvider(context.ml!.mlClient.callAsCurrentUser); @@ -132,7 +132,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat // follow-up import calls to just add additional data will include the `id` of the created // index, we'll ignore those and don't increment the counter. if (id === undefined) { - await incrementFileDataVisualizerIndexCreationCount(context.core.savedObjects.client); + await updateTelemetry(); } const result = await importData(