Skip to content

Commit

Permalink
[Spaces] - basic telemetry (#20581)
Browse files Browse the repository at this point in the history
Introduces basic telemetry for Spaces. Implementation inspired by Reporting's telemetry collector.

Reports the following:

`available`: Indicates of the current license allows for Spaces
`enabled`: Indicates if Spaces is enabled (also implies it is available)
`count`: The number of Spaces this installation has configured

Fixes #19260
  • Loading branch information
legrego committed Sep 4, 2018
1 parent 3e1c1fa commit 278f7b8
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class BulkUploader {
throw new Error('interval number of milliseconds is required');
}

this._timer = null;
this._timer = null;
this._interval = interval;
this._log = {
debug: message => server.log(['debug', ...LOGGING_TAGS], message),
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/spaces/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ export const SPACE_SEARCH_COUNT_THRESHOLD = 8;
* The maximum number of characters allowed in the Space Avatar's initials
*/
export const MAX_SPACE_INITIALS = 2;

/**
* The type name used within the Monitoring index to publish spaces stats.
* @type {string}
*/
export const KIBANA_SPACES_STATS_TYPE = 'spaces';
4 changes: 4 additions & 0 deletions x-pack/plugins/spaces/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { initSpacesRequestInterceptors } from './server/lib/space_request_interc
import { createDefaultSpace } from './server/lib/create_default_space';
import { createSpacesService } from './server/lib/create_spaces_service';
import { getActiveSpace } from './server/lib/get_active_space';
import { getSpacesUsageCollector } from './server/lib/get_spaces_usage_collector';
import { wrapError } from './server/lib/errors';
import mappings from './mappings.json';
import { spacesSavedObjectsClientWrapperFactory } from './server/lib/saved_objects_client/saved_objects_client_wrapper_factory';
Expand Down Expand Up @@ -90,5 +91,8 @@ export const spaces = (kibana) => new kibana.Plugin({
initSpacesApi(server);

initSpacesRequestInterceptors(server);

// Register a function with server to manage the collection of usage stats
server.usage.collectorSet.register(getSpacesUsageCollector(server));
}
});
75 changes: 75 additions & 0 deletions x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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 { KIBANA_SPACES_STATS_TYPE } from '../../common/constants';
import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants';

/**
*
* @param callCluster
* @param server
* @param {boolean} spacesAvailable
* @param withinDayRange
* @return {ReportingUsageStats}
*/
async function getSpacesUsage(callCluster, server, spacesAvailable) {
if (!spacesAvailable) return {};

const { getSavedObjectsRepository } = server.savedObjects;

const savedObjectsRepository = getSavedObjectsRepository(callCluster);

const { saved_objects: spaces } = await savedObjectsRepository.find({ type: 'space' });

return {
count: spaces.length,
};
}

/*
* @param {Object} server
* @return {Object} kibana usage stats type collection object
*/
export function getSpacesUsageCollector(server) {
const { collectorSet } = server.usage;
return collectorSet.makeUsageCollector({
type: KIBANA_SPACES_STATS_TYPE,
fetch: async callCluster => {
const xpackInfo = server.plugins.xpack_main.info;
const config = server.config();
const available = xpackInfo && xpackInfo.isAvailable(); // some form of spaces is available for all valid licenses
const enabled = config.get('xpack.spaces.enabled');
const spacesAvailableAndEnabled = available && enabled;

const usageStats = await getSpacesUsage(callCluster, server, spacesAvailableAndEnabled);

return {
available,
enabled: spacesAvailableAndEnabled, // similar behavior as _xpack API in ES
...usageStats,
};
},

/*
* Format the response data into a model for internal upload
* 1. Make this data part of the "kibana_stats" type
* 2. Organize the payload in the usage.xpack.spaces namespace of the data payload
*/
formatForBulkUpload: result => {
return {
type: KIBANA_STATS_TYPE_MONITORING,
payload: {
usage: {
xpack: {
spaces: result
}
}
}
};

}
});
}
149 changes: 149 additions & 0 deletions x-pack/plugins/spaces/server/lib/get_spaces_usage_collector.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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 { getSpacesUsageCollector } from './get_spaces_usage_collector';

function getServerMock(customization) {
class MockUsageCollector {
constructor(_server, { fetch }) {
this.fetch = fetch;
}
}

const getLicenseCheckResults = jest.fn().mockReturnValue({});
const defaultServerMock = {
plugins: {
security: {
isAuthenticated: jest.fn().mockReturnValue(true)
},
xpack_main: {
info: {
isAvailable: jest.fn().mockReturnValue(true),
feature: () => ({
getLicenseCheckResults
}),
license: {
isOneOf: jest.fn().mockReturnValue(false),
getType: jest.fn().mockReturnValue('platinum'),
},
toJSON: () => ({ b: 1 })
}
}
},
expose: () => { },
log: () => { },
config: () => ({
get: key => {
if (key === 'xpack.spaces.enabled') {
return true;
}
}
}),
usage: {
collectorSet: {
makeUsageCollector: options => {
return new MockUsageCollector(this, options);
}
}
},
savedObjects: {
getSavedObjectsRepository: jest.fn(() => {
return {
find() {
return {
saved_objects: ['a', 'b']
};
}
};
})
}
};
return Object.assign(defaultServerMock, customization);
}

test('sets enabled to false when spaces is turned off', async () => {
const mockConfigGet = jest.fn(key => {
if (key === 'xpack.spaces.enabled') {
return false;
} else if (key.indexOf('xpack.spaces') >= 0) {
throw new Error('Unknown config key!');
}
});
const serverMock = getServerMock({ config: () => ({ get: mockConfigGet }) });
const callClusterMock = jest.fn();
const { fetch: getSpacesUsage } = getSpacesUsageCollector(serverMock);
const usageStats = await getSpacesUsage(callClusterMock);
expect(usageStats.enabled).toBe(false);
});

describe('with a basic license', async () => {
let usageStats;
beforeAll(async () => {
const serverWithBasicLicenseMock = getServerMock();
serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = jest.fn().mockReturnValue('basic');
const callClusterMock = jest.fn(() => Promise.resolve({}));
const { fetch: getSpacesUsage } = getSpacesUsageCollector(serverWithBasicLicenseMock);
usageStats = await getSpacesUsage(callClusterMock);
});

test('sets enabled to true', async () => {
expect(usageStats.enabled).toBe(true);
});

test('sets available to true', async () => {
expect(usageStats.available).toBe(true);
});

test('sets the number of spaces', async () => {
expect(usageStats.count).toBe(2);
});
});

describe('with no license', async () => {
let usageStats;
beforeAll(async () => {
const serverWithNoLicenseMock = getServerMock();
serverWithNoLicenseMock.plugins.xpack_main.info.isAvailable = jest.fn().mockReturnValue(false);
const callClusterMock = jest.fn(() => Promise.resolve({}));
const { fetch: getSpacesUsage } = getSpacesUsageCollector(serverWithNoLicenseMock);
usageStats = await getSpacesUsage(callClusterMock);
});

test('sets enabled to false', async () => {
expect(usageStats.enabled).toBe(false);
});

test('sets available to false', async () => {
expect(usageStats.available).toBe(false);
});

test('does not set the number of spaces', async () => {
expect(usageStats.count).toBeUndefined();
});
});

describe('with platinum license', async () => {
let usageStats;
beforeAll(async () => {
const serverWithPlatinumLicenseMock = getServerMock();
serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = jest.fn().mockReturnValue('platinum');
const callClusterMock = jest.fn(() => Promise.resolve({}));
const { fetch: getSpacesUsage } = getSpacesUsageCollector(serverWithPlatinumLicenseMock);
usageStats = await getSpacesUsage(callClusterMock);
});

test('sets enabled to true', async () => {
expect(usageStats.enabled).toBe(true);
});

test('sets available to true', async () => {
expect(usageStats.available).toBe(true);
});

test('sets the number of spaces', async () => {
expect(usageStats.count).toBe(2);
});
});

0 comments on commit 278f7b8

Please sign in to comment.