Skip to content

Commit

Permalink
Telemetry for Visualizations by type (#28793)
Browse files Browse the repository at this point in the history
* task runner and usage collector for visualizations by type

* type is always just "visualization"

* drop the I- prefix for interfaces

* bug fixes

* ts fix

* comment perfection

* just usage.

* const for task numworkers

* use mapValues

* get next midnight module

* move to oss_telemtry

* test fix

* errMessage.includes(NotInitialized)
  • Loading branch information
tsullivan authored Jan 30, 2019
1 parent 9eef63f commit b379751
Show file tree
Hide file tree
Showing 14 changed files with 612 additions and 0 deletions.
2 changes: 2 additions & 0 deletions x-pack/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { remoteClusters } from './plugins/remote_clusters';
import { crossClusterReplication } from './plugins/cross_cluster_replication';
import { upgradeAssistant } from './plugins/upgrade_assistant';
import { uptime } from './plugins/uptime';
import { ossTelemetry } from './plugins/oss_telemetry';

module.exports = function (kibana) {
return [
Expand Down Expand Up @@ -69,5 +70,6 @@ module.exports = function (kibana) {
crossClusterReplication(kibana),
upgradeAssistant(kibana),
uptime(kibana),
ossTelemetry(kibana),
];
};
11 changes: 11 additions & 0 deletions x-pack/plugins/oss_telemetry/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 const PLUGIN_ID = 'oss_telemetry'; // prefix used for registering properties with services from this plugin
export const VIS_TELEMETRY_TASK = 'vis_telemetry'; // suffix for the _id of our task instance, which must be `get`-able
export const VIS_USAGE_TYPE = 'visualization_types'; // suffix for the properties of data registered with the usage service

export const VIS_TELEMETRY_TASK_NUM_WORKERS = 10; // by default it's 100% their workers. Users can scale up and set task manager's numWorkers higher for other tasks to be able to run concurrently in a single Kibana instance with this one
66 changes: 66 additions & 0 deletions x-pack/plugins/oss_telemetry/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 interface VisState {
type: string;
}

export interface Visualization {
visState: string;
}

export interface SavedObjectDoc {
_id: string;
_source: {
visualization: Visualization;
type: string;
};
}

export interface ESQueryResponse {
hits: {
hits: SavedObjectDoc[];
};
}

export interface TaskInstance {
state: {
runs: number;
stats: any;
};
error?: any;
}

export interface HapiServer {
taskManager: {
registerTaskDefinitions: (opts: any) => void;
schedule: (opts: any) => Promise<void>;
fetch: (
opts: any
) => Promise<{
docs: TaskInstance[];
}>;
};
plugins: {
xpack_main: any;
elasticsearch: {
getCluster: (
cluster: string
) => {
callWithInternalUser: () => Promise<ESQueryResponse>;
};
};
};
usage: {
collectorSet: {
register: (collector: any) => void;
makeUsageCollector: (collectorOpts: any) => void;
};
};
config: () => {
get: (prop: string) => any;
};
}
22 changes: 22 additions & 0 deletions x-pack/plugins/oss_telemetry/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 { registerCollectors } from './server/lib/collectors';
import { registerTasks, scheduleTasks } from './server/lib/tasks';
import { PLUGIN_ID } from './constants';

export const ossTelemetry = (kibana) => {
return new kibana.Plugin({
id: PLUGIN_ID,
require: ['elasticsearch', 'xpack_main', 'task_manager'],

init(server) {
registerCollectors(server);
registerTasks(server);
scheduleTasks(server);
}
});
};
12 changes: 12 additions & 0 deletions x-pack/plugins/oss_telemetry/server/lib/collectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 { HapiServer } from '../../../';
import { registerVisualizationsCollector } from './visualizations/register_usage_collector';

export function registerCollectors(server: HapiServer) {
registerVisualizationsCollector(server);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 { HapiServer } from '../../../../';
import {
getMockCallWithInternal,
getMockKbnServer,
getMockTaskFetch,
} from '../../../../test_utils';
import { getUsageCollector } from './get_usage_collector';

describe('getVisualizationsCollector#fetch', () => {
let mockKbnServer: HapiServer;

beforeEach(() => {
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), getMockTaskFetch());
});

test('can return empty stats', async () => {
const { type, fetch } = getUsageCollector(mockKbnServer);
expect(type).toBe('visualization_types');
const fetchResult = await fetch();
expect(fetchResult).toEqual({});
});

test('provides known stats', async () => {
const mockTaskFetch = getMockTaskFetch([
{
state: {
runs: 1,
stats: { comic_books: { total: 16, max: 12, min: 2, avg: 6 } },
},
},
]);
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch);

const { type, fetch } = getUsageCollector(mockKbnServer);
expect(type).toBe('visualization_types');
const fetchResult = await fetch();
expect(fetchResult).toEqual({ comic_books: { avg: 6, max: 12, min: 2, total: 16 } });
});

describe('Error handling', () => {
test('Silently handles Task Manager NotInitialized', async () => {
const mockTaskFetch = sinon.stub();
mockTaskFetch.rejects(
new Error('NotInitialized taskManager is still waiting for plugins to load')
);
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch);

const { fetch } = getUsageCollector(mockKbnServer);
await expect(fetch()).resolves.toBe(undefined);
});
// In real life, the CollectorSet calls fetch and handles errors
test('defers the errors', async () => {
const mockTaskFetch = sinon.stub();
mockTaskFetch.rejects(new Error('BOOM'));
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch);

const { fetch } = getUsageCollector(mockKbnServer);
await expect(fetch()).rejects.toMatchObject(new Error('BOOM'));
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { get } from 'lodash';
import { HapiServer } from '../../../../';
import { PLUGIN_ID, VIS_TELEMETRY_TASK, VIS_USAGE_TYPE } from '../../../../constants';

export function getUsageCollector(server: HapiServer) {
const { taskManager } = server;
return {
type: VIS_USAGE_TYPE,
fetch: async () => {
let docs;
try {
({ docs } = await taskManager.fetch({
query: { bool: { filter: { term: { _id: `${PLUGIN_ID}-${VIS_TELEMETRY_TASK}` } } } },
}));
} catch (err) {
const errMessage = err && err.message ? err.message : err.toString();
/*
* The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the task manager
* has to wait for all plugins to initialize first.
* It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error)
*/
if (errMessage.includes('NotInitialized')) {
docs = {};
} else {
throw err;
}
}

// get the accumulated state from the recurring task
return get(docs, '[0].state.stats');
},
};
}
Original file line number Diff line number Diff line change
@@ -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 { HapiServer } from '../../../../';
import { getUsageCollector } from './get_usage_collector';

export function registerVisualizationsCollector(server: HapiServer): void {
const { usage } = server;
const collector = usage.collectorSet.makeUsageCollector(getUsageCollector(server));
usage.collectorSet.register(collector);
}
19 changes: 19 additions & 0 deletions x-pack/plugins/oss_telemetry/server/lib/get_next_midnight.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 moment from 'moment';
import { getNextMidnight } from './get_next_midnight';

describe('getNextMidnight', () => {
test('Returns the next time and date of midnight as an iso string', () => {
const nextMidnightMoment = moment()
.add(1, 'days')
.startOf('day')
.toISOString();

expect(getNextMidnight()).toEqual(nextMidnightMoment);
});
});
12 changes: 12 additions & 0 deletions x-pack/plugins/oss_telemetry/server/lib/get_next_midnight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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 function getNextMidnight() {
const nextMidnight = new Date();
nextMidnight.setHours(0, 0, 0, 0);
nextMidnight.setDate(nextMidnight.getDate() + 1);
return nextMidnight.toISOString();
}
39 changes: 39 additions & 0 deletions x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { HapiServer } from '../../../';
import { PLUGIN_ID, VIS_TELEMETRY_TASK, VIS_TELEMETRY_TASK_NUM_WORKERS } from '../../../constants';
import { visualizationsTaskRunner } from './visualizations/task_runner';

export function registerTasks(server: HapiServer) {
const { taskManager } = server;

taskManager.registerTaskDefinitions({
[VIS_TELEMETRY_TASK]: {
title: 'X-Pack telemetry calculator for Visualizations',
type: VIS_TELEMETRY_TASK,
numWorkers: VIS_TELEMETRY_TASK_NUM_WORKERS, // by default it's 100% their workers
createTaskRunner({ taskInstance, kbnServer }: { kbnServer: any; taskInstance: any }) {
return {
run: visualizationsTaskRunner(taskInstance, kbnServer),
};
},
},
});
}

export function scheduleTasks(server: HapiServer) {
const { taskManager } = server;
const { kbnServer } = server.plugins.xpack_main.status.plugin;

kbnServer.afterPluginsInit(() => {
taskManager.schedule({
id: `${PLUGIN_ID}-${VIS_TELEMETRY_TASK}`,
taskType: VIS_TELEMETRY_TASK,
state: { stats: {}, runs: 0 },
});
});
}
Loading

0 comments on commit b379751

Please sign in to comment.