Skip to content

Commit df19d7e

Browse files
authored
Telemetry for Visualizations by type (#28793) (#29625)
* 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)
1 parent 33e6b48 commit df19d7e

File tree

14 files changed

+612
-0
lines changed

14 files changed

+612
-0
lines changed

x-pack/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { remoteClusters } from './plugins/remote_clusters';
3535
import { crossClusterReplication } from './plugins/cross_cluster_replication';
3636
import { upgradeAssistant } from './plugins/upgrade_assistant';
3737
import { uptime } from './plugins/uptime';
38+
import { ossTelemetry } from './plugins/oss_telemetry';
3839

3940
module.exports = function (kibana) {
4041
return [
@@ -69,5 +70,6 @@ module.exports = function (kibana) {
6970
crossClusterReplication(kibana),
7071
upgradeAssistant(kibana),
7172
uptime(kibana),
73+
ossTelemetry(kibana),
7274
];
7375
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export const PLUGIN_ID = 'oss_telemetry'; // prefix used for registering properties with services from this plugin
8+
export const VIS_TELEMETRY_TASK = 'vis_telemetry'; // suffix for the _id of our task instance, which must be `get`-able
9+
export const VIS_USAGE_TYPE = 'visualization_types'; // suffix for the properties of data registered with the usage service
10+
11+
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
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export interface VisState {
8+
type: string;
9+
}
10+
11+
export interface Visualization {
12+
visState: string;
13+
}
14+
15+
export interface SavedObjectDoc {
16+
_id: string;
17+
_source: {
18+
visualization: Visualization;
19+
type: string;
20+
};
21+
}
22+
23+
export interface ESQueryResponse {
24+
hits: {
25+
hits: SavedObjectDoc[];
26+
};
27+
}
28+
29+
export interface TaskInstance {
30+
state: {
31+
runs: number;
32+
stats: any;
33+
};
34+
error?: any;
35+
}
36+
37+
export interface HapiServer {
38+
taskManager: {
39+
registerTaskDefinitions: (opts: any) => void;
40+
schedule: (opts: any) => Promise<void>;
41+
fetch: (
42+
opts: any
43+
) => Promise<{
44+
docs: TaskInstance[];
45+
}>;
46+
};
47+
plugins: {
48+
xpack_main: any;
49+
elasticsearch: {
50+
getCluster: (
51+
cluster: string
52+
) => {
53+
callWithInternalUser: () => Promise<ESQueryResponse>;
54+
};
55+
};
56+
};
57+
usage: {
58+
collectorSet: {
59+
register: (collector: any) => void;
60+
makeUsageCollector: (collectorOpts: any) => void;
61+
};
62+
};
63+
config: () => {
64+
get: (prop: string) => any;
65+
};
66+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { registerCollectors } from './server/lib/collectors';
8+
import { registerTasks, scheduleTasks } from './server/lib/tasks';
9+
import { PLUGIN_ID } from './constants';
10+
11+
export const ossTelemetry = (kibana) => {
12+
return new kibana.Plugin({
13+
id: PLUGIN_ID,
14+
require: ['elasticsearch', 'xpack_main', 'task_manager'],
15+
16+
init(server) {
17+
registerCollectors(server);
18+
registerTasks(server);
19+
scheduleTasks(server);
20+
}
21+
});
22+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { HapiServer } from '../../../';
8+
import { registerVisualizationsCollector } from './visualizations/register_usage_collector';
9+
10+
export function registerCollectors(server: HapiServer) {
11+
registerVisualizationsCollector(server);
12+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import sinon from 'sinon';
8+
import { HapiServer } from '../../../../';
9+
import {
10+
getMockCallWithInternal,
11+
getMockKbnServer,
12+
getMockTaskFetch,
13+
} from '../../../../test_utils';
14+
import { getUsageCollector } from './get_usage_collector';
15+
16+
describe('getVisualizationsCollector#fetch', () => {
17+
let mockKbnServer: HapiServer;
18+
19+
beforeEach(() => {
20+
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), getMockTaskFetch());
21+
});
22+
23+
test('can return empty stats', async () => {
24+
const { type, fetch } = getUsageCollector(mockKbnServer);
25+
expect(type).toBe('visualization_types');
26+
const fetchResult = await fetch();
27+
expect(fetchResult).toEqual({});
28+
});
29+
30+
test('provides known stats', async () => {
31+
const mockTaskFetch = getMockTaskFetch([
32+
{
33+
state: {
34+
runs: 1,
35+
stats: { comic_books: { total: 16, max: 12, min: 2, avg: 6 } },
36+
},
37+
},
38+
]);
39+
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch);
40+
41+
const { type, fetch } = getUsageCollector(mockKbnServer);
42+
expect(type).toBe('visualization_types');
43+
const fetchResult = await fetch();
44+
expect(fetchResult).toEqual({ comic_books: { avg: 6, max: 12, min: 2, total: 16 } });
45+
});
46+
47+
describe('Error handling', () => {
48+
test('Silently handles Task Manager NotInitialized', async () => {
49+
const mockTaskFetch = sinon.stub();
50+
mockTaskFetch.rejects(
51+
new Error('NotInitialized taskManager is still waiting for plugins to load')
52+
);
53+
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch);
54+
55+
const { fetch } = getUsageCollector(mockKbnServer);
56+
await expect(fetch()).resolves.toBe(undefined);
57+
});
58+
// In real life, the CollectorSet calls fetch and handles errors
59+
test('defers the errors', async () => {
60+
const mockTaskFetch = sinon.stub();
61+
mockTaskFetch.rejects(new Error('BOOM'));
62+
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch);
63+
64+
const { fetch } = getUsageCollector(mockKbnServer);
65+
await expect(fetch()).rejects.toMatchObject(new Error('BOOM'));
66+
});
67+
});
68+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { get } from 'lodash';
8+
import { HapiServer } from '../../../../';
9+
import { PLUGIN_ID, VIS_TELEMETRY_TASK, VIS_USAGE_TYPE } from '../../../../constants';
10+
11+
export function getUsageCollector(server: HapiServer) {
12+
const { taskManager } = server;
13+
return {
14+
type: VIS_USAGE_TYPE,
15+
fetch: async () => {
16+
let docs;
17+
try {
18+
({ docs } = await taskManager.fetch({
19+
query: { bool: { filter: { term: { _id: `${PLUGIN_ID}-${VIS_TELEMETRY_TASK}` } } } },
20+
}));
21+
} catch (err) {
22+
const errMessage = err && err.message ? err.message : err.toString();
23+
/*
24+
* The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the task manager
25+
* has to wait for all plugins to initialize first.
26+
* It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error)
27+
*/
28+
if (errMessage.includes('NotInitialized')) {
29+
docs = {};
30+
} else {
31+
throw err;
32+
}
33+
}
34+
35+
// get the accumulated state from the recurring task
36+
return get(docs, '[0].state.stats');
37+
},
38+
};
39+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { HapiServer } from '../../../../';
8+
import { getUsageCollector } from './get_usage_collector';
9+
10+
export function registerVisualizationsCollector(server: HapiServer): void {
11+
const { usage } = server;
12+
const collector = usage.collectorSet.makeUsageCollector(getUsageCollector(server));
13+
usage.collectorSet.register(collector);
14+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import moment from 'moment';
8+
import { getNextMidnight } from './get_next_midnight';
9+
10+
describe('getNextMidnight', () => {
11+
test('Returns the next time and date of midnight as an iso string', () => {
12+
const nextMidnightMoment = moment()
13+
.add(1, 'days')
14+
.startOf('day')
15+
.toISOString();
16+
17+
expect(getNextMidnight()).toEqual(nextMidnightMoment);
18+
});
19+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export function getNextMidnight() {
8+
const nextMidnight = new Date();
9+
nextMidnight.setHours(0, 0, 0, 0);
10+
nextMidnight.setDate(nextMidnight.getDate() + 1);
11+
return nextMidnight.toISOString();
12+
}

0 commit comments

Comments
 (0)