diff --git a/x-pack/legacy/plugins/code/common/constants.ts b/x-pack/legacy/plugins/code/common/constants.ts index 652281ce9c252..ec7356bccb3f7 100644 --- a/x-pack/legacy/plugins/code/common/constants.ts +++ b/x-pack/legacy/plugins/code/common/constants.ts @@ -5,3 +5,4 @@ */ export const APP_TITLE = 'Code (Beta)'; +export const APP_USAGE_TYPE = 'code'; diff --git a/x-pack/legacy/plugins/code/model/index.ts b/x-pack/legacy/plugins/code/model/index.ts index e662ee51a8079..5db839932a247 100644 --- a/x-pack/legacy/plugins/code/model/index.ts +++ b/x-pack/legacy/plugins/code/model/index.ts @@ -10,4 +10,3 @@ export * from './repository'; export * from './task'; export * from './lsp'; export * from './workspace'; -export * from './socket'; diff --git a/x-pack/legacy/plugins/code/model/socket.ts b/x-pack/legacy/plugins/code/model/usage_telemetry_metrics.ts similarity index 57% rename from x-pack/legacy/plugins/code/model/socket.ts rename to x-pack/legacy/plugins/code/model/usage_telemetry_metrics.ts index f29659e6fd937..c9e77c4ec0631 100644 --- a/x-pack/legacy/plugins/code/model/socket.ts +++ b/x-pack/legacy/plugins/code/model/usage_telemetry_metrics.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum SocketKind { - CLONE_PROGRESS = 'clone-progress', - DELETE_PROGRESS = 'delete-progress', - INDEX_PROGRESS = 'index-progress', - INSTALL_PROGRESS = 'install-progress', +export enum CodeUsageMetrics { + ENABLED = 'enabled', + REPOSITORIES = 'repositories', + LANGUAGE_SERVERS = 'langserver', } diff --git a/x-pack/legacy/plugins/code/public/components/shared/icons.tsx b/x-pack/legacy/plugins/code/public/components/shared/icons.tsx index 4194a22c3396d..1ee9c2faaf8de 100644 --- a/x-pack/legacy/plugins/code/public/components/shared/icons.tsx +++ b/x-pack/legacy/plugins/code/public/components/shared/icons.tsx @@ -62,8 +62,8 @@ export const CtagsIcon = () => ( diff --git a/x-pack/legacy/plugins/code/public/lib/usage_collector.ts b/x-pack/legacy/plugins/code/public/lib/usage_collector.ts new file mode 100644 index 0000000000000..5b63227ee4655 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/lib/usage_collector.ts @@ -0,0 +1,14 @@ +/* + * 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 { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { APP_USAGE_TYPE } from '../../common/constants'; + +export const trackUiAction = createUiStatsReporter(APP_USAGE_TYPE); +export { METRIC_TYPE }; diff --git a/x-pack/legacy/plugins/code/server/plugin.ts b/x-pack/legacy/plugins/code/server/plugin.ts index 87680837cb864..bae89cc5f1679 100644 --- a/x-pack/legacy/plugins/code/server/plugin.ts +++ b/x-pack/legacy/plugins/code/server/plugin.ts @@ -55,6 +55,7 @@ import { initQueue } from './init_queue'; import { initWorkers } from './init_workers'; import { ClusterNodeAdapter } from './distributed/cluster/cluster_node_adapter'; import { NodeRepositoriesService } from './distributed/cluster/node_repositories_service'; +import { initCodeUsageCollector } from './usage_collector'; export class CodePlugin { private isCodeNode = false; @@ -241,6 +242,10 @@ export class CodePlugin { await tryMigrateIndices(esClient, this.log); this.initRoutes(server, codeServices, repoIndexInitializerFactory, repoConfigController); + + // TODO: extend the usage collection to cluster mode. + initCodeUsageCollector(server, esClient, lspService); + return codeServices; } diff --git a/x-pack/legacy/plugins/code/server/usage_collector.test.ts b/x-pack/legacy/plugins/code/server/usage_collector.test.ts new file mode 100644 index 0000000000000..46f0b3b3f765f --- /dev/null +++ b/x-pack/legacy/plugins/code/server/usage_collector.test.ts @@ -0,0 +1,131 @@ +/* + * 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 sinon from 'sinon'; + +import { LanguageServerStatus } from '../common/language_server'; +import { RepositoryReservedField } from './indexer/schema'; +import { EsClient } from './lib/esqueue'; +import { LspService } from './lsp/lsp_service'; +import { emptyAsyncFunc } from './test_utils'; +import * as usageCollector from './usage_collector'; + +const esClient = { + search: emptyAsyncFunc, +}; + +const lspService = { + languageServerStatus: emptyAsyncFunc, +}; + +const createSearchSpy = (): sinon.SinonSpy => { + return sinon.fake.returns( + Promise.resolve({ + hits: { + hits: [ + { + _source: { + [RepositoryReservedField]: { + uri: 'github.com/elastic/code1', + }, + }, + }, + { + _source: { + [RepositoryReservedField]: { + uri: 'github.com/elastic/code2', + }, + }, + }, + ], + }, + }) + ); +}; + +describe('Code Usage Collector', () => { + let makeUsageCollectorStub: any; + let registerStub: any; + let serverStub: any; + let callClusterStub: any; + let languageServerStatusStub: any; + let searchStub: any; + + beforeEach(() => { + makeUsageCollectorStub = sinon.spy(); + registerStub = sinon.stub(); + serverStub = { + usage: { + collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub }, + register: {}, + }, + }; + callClusterStub = sinon.stub(); + + searchStub = createSearchSpy(); + esClient.search = searchStub; + + languageServerStatusStub = sinon.stub(); + languageServerStatusStub.withArgs('TypeScript').returns(LanguageServerStatus.READY); + languageServerStatusStub.withArgs('Java').returns(LanguageServerStatus.READY); + languageServerStatusStub.withArgs('Ctags').returns(LanguageServerStatus.READY); + languageServerStatusStub.withArgs('Go').returns(LanguageServerStatus.NOT_INSTALLED); + lspService.languageServerStatus = languageServerStatusStub; + }); + + describe('initCodeUsageCollector', () => { + it('should call collectorSet.register', () => { + usageCollector.initCodeUsageCollector( + serverStub, + esClient as EsClient, + (lspService as any) as LspService + ); + expect(registerStub.calledOnce).toBeTruthy(); + }); + + it('should call makeUsageCollector with type = code', () => { + usageCollector.initCodeUsageCollector( + serverStub, + esClient as EsClient, + (lspService as any) as LspService + ); + expect(makeUsageCollectorStub.calledOnce).toBeTruthy(); + expect(makeUsageCollectorStub.getCall(0).args[0].type).toBe('code'); + }); + + it('should return correct stats', async () => { + usageCollector.initCodeUsageCollector( + serverStub, + esClient as EsClient, + (lspService as any) as LspService + ); + const codeStats = await makeUsageCollectorStub.getCall(0).args[0].fetch(callClusterStub); + expect(callClusterStub.notCalled).toBeTruthy(); + expect(codeStats).toEqual({ + enabled: 1, + repositories: 2, + langserver: [ + { + enabled: 1, + key: 'TypeScript', + }, + { + enabled: 1, + key: 'Java', + }, + { + enabled: 1, + key: 'Ctags', + }, + { + enabled: 0, + key: 'Go', + }, + ], + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/code/server/usage_collector.ts b/x-pack/legacy/plugins/code/server/usage_collector.ts new file mode 100644 index 0000000000000..0844235821bbb --- /dev/null +++ b/x-pack/legacy/plugins/code/server/usage_collector.ts @@ -0,0 +1,52 @@ +/* + * 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 { ServerFacade } from '../'; +import { APP_USAGE_TYPE } from '../common/constants'; +import { LanguageServerStatus } from '../common/language_server'; +import { CodeUsageMetrics } from '../model/usage_telemetry_metrics'; +import { EsClient } from './lib/esqueue'; +import { RepositoryObjectClient } from './search'; +import { LspService } from './lsp/lsp_service'; +import { LanguageServers, LanguageServerDefinition } from './lsp/language_servers'; + +export async function fetchCodeUsageMetrics(client: EsClient, lspService: LspService) { + const repositoryObjectClient: RepositoryObjectClient = new RepositoryObjectClient(client); + const allRepos = await repositoryObjectClient.getAllRepositories(); + const langServerEnabled = async (name: string) => { + const status = await lspService.languageServerStatus(name); + return status !== LanguageServerStatus.NOT_INSTALLED ? 1 : 0; + }; + + const langServersEnabled = await Promise.all( + LanguageServers.map(async (langServer: LanguageServerDefinition) => { + return { + key: langServer.name, + enabled: await langServerEnabled(langServer.name), + }; + }) + ); + + return { + [CodeUsageMetrics.ENABLED]: 1, + [CodeUsageMetrics.REPOSITORIES]: allRepos.length, + [CodeUsageMetrics.LANGUAGE_SERVERS]: langServersEnabled, + }; +} + +export function initCodeUsageCollector( + server: ServerFacade, + client: EsClient, + lspService: LspService +) { + const codeUsageCollector = server.usage.collectorSet.makeUsageCollector({ + type: APP_USAGE_TYPE, + isReady: () => true, + fetch: async () => fetchCodeUsageMetrics(client, lspService), + }); + + server.usage.collectorSet.register(codeUsageCollector); +}