Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spaces] - basic telemetry #20581

Merged
merged 12 commits into from
Sep 4, 2018
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ const getInitial = () => {
}
}
],
[
{ 'index': { '_type': 'spaces_stats' } },
{
'available': true,
'enabled': true,
'count': 1
}
],
[
{ 'index': { '_type': 'kibana_settings' } },
{ 'xpack': { 'defaultAdminEmail': 'tim@elastic.co' } }
Expand Down Expand Up @@ -124,6 +132,11 @@ const getResult = () => {
'available': true,
'count': 30,
}
},
'spaces': {
'available': true,
'enabled': true,
'count': 1
}
}
}
Expand Down Expand Up @@ -175,36 +188,36 @@ describe('Collector Types Combiner', () => {
it('provides combined stats/usage data', () => {
// default gives all the data types
const initial = getInitial();
const trimmedInitial = [ initial[0], initial[1], initial[2] ]; // just stats, usage and reporting, no settings
const trimmedInitial = [initial[0], initial[1], initial[2], initial[3]]; // just stats, usage and reporting, no settings
const combiner = getCollectorTypesCombiner(kbnServerMock, configMock, sourceKibanaMock);
const result = combiner(trimmedInitial);
const expectedResult = getResult();
const trimmedExpectedResult = [ expectedResult[0] ]; // single combined item
const trimmedExpectedResult = [expectedResult[0]]; // single combined item
expect(result).to.eql(trimmedExpectedResult);
});
});
describe('with usage data missing', () => {
it('provides settings, and stats data', () => {
// default gives all the data types
const initial = getInitial();
const trimmedInitial = [ initial[0], initial[3] ]; // just stats and settings, no usage or reporting
const trimmedInitial = [initial[0], initial[4]]; // just stats and settings, no usage or reporting
const combiner = getCollectorTypesCombiner(kbnServerMock, configMock, sourceKibanaMock);
const result = combiner(trimmedInitial);
const expectedResult = getResult();
delete expectedResult[0][1].usage; // usage stats should not be present in the result
const trimmedExpectedResult = [ expectedResult[0], expectedResult[1] ];
const trimmedExpectedResult = [expectedResult[0], expectedResult[1]];
expect(result).to.eql(trimmedExpectedResult);
});
});
describe('with stats data missing', () => {
it('provides settings data', () => {
// default gives all the data types
const initial = getInitial();
const trimmedInitial = [ initial[3] ]; // just settings
const trimmedInitial = [initial[4]]; // just settings
const combiner = getCollectorTypesCombiner(kbnServerMock, configMock, sourceKibanaMock);
const result = combiner(trimmedInitial);
const expectedResult = getResult();
const trimmedExpectedResult = [ expectedResult[1] ]; // just settings
const trimmedExpectedResult = [expectedResult[1]]; // just settings
expect(result).to.eql(trimmedExpectedResult);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
KIBANA_USAGE_TYPE,
} from '../../../common/constants';
import { KIBANA_REPORTING_TYPE } from '../../../../reporting/common/constants';
import { KIBANA_SPACES_MONITORING_TYPE } from '../../../../spaces/common/constants';
import { sourceKibana } from './source_kibana';

/*
Expand Down Expand Up @@ -39,17 +40,20 @@ export function getCollectorTypesCombiner(kbnServer, config, _sourceKibana = sou

// kibana usage and stats
let statsResult;
const [ statsHeader, statsPayload ] = findItem(KIBANA_STATS_TYPE);
const [ reportingHeader, reportingPayload ] = findItem(KIBANA_REPORTING_TYPE);
const [statsHeader, statsPayload] = findItem(KIBANA_STATS_TYPE);
const [reportingHeader, reportingPayload] = findItem(KIBANA_REPORTING_TYPE);
const [spacesHeader, spacesPayload] = findItem(KIBANA_SPACES_MONITORING_TYPE);

// sourceKibana uses "host" from the kibana stats payload
const host = get(statsPayload, 'host');
const kibana = _sourceKibana(kbnServer, config, host);

if (statsHeader && statsPayload) {
const [ usageHeader, usagePayload ] = findItem(KIBANA_USAGE_TYPE);
const [usageHeader, usagePayload] = findItem(KIBANA_USAGE_TYPE);
const kibanaUsage = (usageHeader && usagePayload) ? usagePayload : null;
const reportingUsage = (reportingHeader && reportingPayload) ? reportingPayload : null; // this is an abstraction leak
const spacesUsage = (spacesHeader && spacesPayload) ? spacesPayload : null; //this is an abstraction leak

statsResult = [
statsHeader,
{
Expand All @@ -63,11 +67,14 @@ export function getCollectorTypesCombiner(kbnServer, config, _sourceKibana = sou
if (reportingUsage) {
set(statsResult, '[1].usage.xpack.reporting', reportingUsage); // this is an abstraction leak
}
if (spacesUsage) {
set(statsResult, '[1].usage.xpack.spaces', spacesUsage); // this is an abstraction leak
}
}

// kibana settings
let settingsResult;
const [ settingsHeader, settingsPayload ] = findItem(KIBANA_SETTINGS_TYPE);
const [settingsHeader, settingsPayload] = findItem(KIBANA_SETTINGS_TYPE);
if (settingsHeader && settingsPayload) {
settingsResult = [
settingsHeader,
Expand All @@ -81,7 +88,7 @@ export function getCollectorTypesCombiner(kbnServer, config, _sourceKibana = sou
// return new payload with the combined data
// adds usage data to stats data
// strips usage out as a top-level type
const result = [ statsResult, settingsResult ];
const result = [statsResult, settingsResult];
Copy link
Member

@tsullivan tsullivan Jul 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole method now correlates to a static method of the BulkUploader class in master: https://github.com/tsullivan/kibana/blob/528cc0d/x-pack/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js#L142


// remove result items that are undefined
return result.filter(Boolean);
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/spaces/common/constants.js
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_MONITORING_TYPE = 'spaces_stats';
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 { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status';
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';

Expand Down Expand Up @@ -78,6 +79,9 @@ export const spaces = (kibana) => new kibana.Plugin({

initSpacesRequestInterceptors(server);

// Register a function with server to manage the collection of usage stats
server.usage.collectorSet.register(getSpacesUsageCollector(server));

await createDefaultSpace(server);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure how this line is part of the telemetry additions

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, good catch! Will remove

}
});
55 changes: 55 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,55 @@
/*
* 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_MONITORING_TYPE } from '../../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_MONITORING_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'); // follow ES behavior: if its not available then its not enabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like we should be able to make this const enabled = config.get('xpack.spaces.enabled') && available and use it instead of spacesAvailable and in the return below.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but then we can't distinguish between enabled and available in the response below. I can try to clarify and clean this up a bit though

const spacesAvailable = available && enabled;

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

return {
available,
enabled: available && enabled, // similar behavior as _xpack API in ES
...usageStats,
};
}
});
}
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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { wrap } from 'boom';
import { callClusterFactory } from '../../../lib/call_cluster_factory';
import { getKibanaUsageCollector } from '../../../../../monitoring/server/kibana_monitoring/collectors';
import { getReportingUsageCollector } from '../../../../../reporting/server/usage';
import { getSpacesUsageCollector } from '../../../../../spaces/server/lib/get_spaces_usage_collector';

export function kibanaStatsRoute(server) {
server.route({
Expand All @@ -24,17 +25,20 @@ export function kibanaStatsRoute(server) {
try {
const kibanaUsageCollector = getKibanaUsageCollector(server);
const reportingUsageCollector = getReportingUsageCollector(server);
const spacesUsageCollector = getSpacesUsageCollector(server);

const [ kibana, reporting ] = await Promise.all([
const [kibana, reporting, spaces] = await Promise.all([
kibanaUsageCollector.fetch(callCluster),
reportingUsageCollector.fetch(callCluster),
spacesUsageCollector.fetch(callCluster),
]);

reply({
kibana,
reporting,
spaces,
});
} catch(err) {
} catch (err) {
req.log(['error'], err);

if (err.isBoom) {
Expand Down