mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Maps] Fix regression preventing maps telemetry from populating & remove task manager logic (#52834)
* Remove task logic. Remove server refs and revise for np. Migrate a few files to ts * Remove unused reference * Update mappings * Test usage collector register * Update api integration tests to include maps now that telemetry is 'normal' (not using task mgr state) * Update integration test to use stack stats * Update integration test to look for 'maps-telemetry' instead of 'maps' * Update jest test to reflect calls to register * Follow the same pattern as other int tests and test reliable nested attribute * Back out np-related changes for separate PR * timeCaptured hasn't changed but for some reason stopped working. Getting iso string fixes issue * Back out file shuffling for separate PR * Remove mappings updates (handled in separate PR) * Review feedback. Move telemetry type constant to constants file * Consolidate imports * Linting fix Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
f08fc201c8
commit
368a894bd2
7 changed files with 46 additions and 322 deletions
|
@ -23,6 +23,7 @@ export const EMS_TILES_VECTOR_TILE_PATH = 'ems/tiles/vector/tile';
|
|||
export const MAP_SAVED_OBJECT_TYPE = 'map';
|
||||
export const APP_ID = 'maps';
|
||||
export const APP_ICON = 'gisApp';
|
||||
export const TELEMETRY_TYPE = 'maps-telemetry';
|
||||
|
||||
export const MAP_APP_PATH = `app/${APP_ID}`;
|
||||
export const GIS_API_PATH = `api/${APP_ID}`;
|
||||
|
|
|
@ -5,10 +5,16 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { EMS_FILE, ES_GEO_FIELD_TYPE, MAP_SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
import {
|
||||
EMS_FILE,
|
||||
ES_GEO_FIELD_TYPE,
|
||||
MAP_SAVED_OBJECT_TYPE,
|
||||
TELEMETRY_TYPE,
|
||||
} from '../../common/constants';
|
||||
|
||||
function getSavedObjectsClient(server, callCluster) {
|
||||
function getSavedObjectsClient(server) {
|
||||
const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects;
|
||||
const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser;
|
||||
const internalRepository = getSavedObjectsRepository(callCluster);
|
||||
return new SavedObjectsClient(internalRepository);
|
||||
}
|
||||
|
@ -79,7 +85,7 @@ export function buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects,
|
|||
// Total count of maps
|
||||
mapsTotalCount: mapsCount,
|
||||
// Time of capture
|
||||
timeCaptured: new Date(),
|
||||
timeCaptured: new Date().toISOString(),
|
||||
attributesPerMap: {
|
||||
// Count of data sources per map
|
||||
dataSourcesCount: {
|
||||
|
@ -115,16 +121,16 @@ async function getIndexPatternSavedObjects(savedObjectsClient) {
|
|||
return _.get(indexPatternSavedObjects, 'saved_objects', []);
|
||||
}
|
||||
|
||||
export async function getMapsTelemetry(server, callCluster) {
|
||||
const savedObjectsClient = getSavedObjectsClient(server, callCluster);
|
||||
export async function getMapsTelemetry(server) {
|
||||
const savedObjectsClient = getSavedObjectsClient(server);
|
||||
const mapSavedObjects = await getMapSavedObjects(savedObjectsClient);
|
||||
const indexPatternSavedObjects = await getIndexPatternSavedObjects(savedObjectsClient);
|
||||
const settings = {
|
||||
showMapVisualizationTypes: server.config().get('xpack.maps.showMapVisualizationTypes'),
|
||||
};
|
||||
const mapsTelemetry = buildMapsTelemetry({ mapSavedObjects, indexPatternSavedObjects, settings });
|
||||
return await savedObjectsClient.create('maps-telemetry', mapsTelemetry, {
|
||||
id: 'maps-telemetry',
|
||||
return await savedObjectsClient.create(TELEMETRY_TYPE, mapsTelemetry, {
|
||||
id: TELEMETRY_TYPE,
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,85 +4,19 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { TASK_ID, scheduleTask, registerMapsTelemetryTask } from './telemetry_task';
|
||||
import { getMapsTelemetry } from './maps_telemetry';
|
||||
import { TELEMETRY_TYPE } from '../../common/constants';
|
||||
|
||||
export function initTelemetryCollection(usageCollection, server) {
|
||||
registerMapsTelemetryTask(server);
|
||||
scheduleTask(server);
|
||||
registerMapsUsageCollector(usageCollection, server);
|
||||
}
|
||||
|
||||
async function isTaskManagerReady(server) {
|
||||
const result = await fetch(server);
|
||||
return result !== null;
|
||||
}
|
||||
|
||||
async function fetch(server) {
|
||||
let docs;
|
||||
const taskManager = server.plugins.task_manager;
|
||||
|
||||
if (!taskManager) {
|
||||
return null;
|
||||
if (!usageCollection) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
({ docs } = await taskManager.fetch({
|
||||
query: {
|
||||
bool: {
|
||||
filter: {
|
||||
term: {
|
||||
_id: `task:${TASK_ID}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
} 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.indexOf('NotInitialized') >= 0) {
|
||||
return null;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const mapsUsageCollector = usageCollection.makeUsageCollector({
|
||||
type: TELEMETRY_TYPE,
|
||||
isReady: () => true,
|
||||
fetch: async () => await getMapsTelemetry(server),
|
||||
});
|
||||
|
||||
return docs;
|
||||
}
|
||||
|
||||
export function buildCollectorObj(server) {
|
||||
let isCollectorReady = false;
|
||||
async function determineIfTaskManagerIsReady() {
|
||||
let isReady = false;
|
||||
try {
|
||||
isReady = await isTaskManagerReady(server);
|
||||
} catch (err) {} // eslint-disable-line
|
||||
|
||||
if (isReady) {
|
||||
isCollectorReady = true;
|
||||
} else {
|
||||
setTimeout(determineIfTaskManagerIsReady, 500);
|
||||
}
|
||||
}
|
||||
determineIfTaskManagerIsReady();
|
||||
|
||||
return {
|
||||
type: 'maps',
|
||||
isReady: () => isCollectorReady,
|
||||
fetch: async () => {
|
||||
const docs = await fetch(server);
|
||||
return _.get(docs, '[0].state.stats');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function registerMapsUsageCollector(usageCollection, server) {
|
||||
const collectorObj = buildCollectorObj(server);
|
||||
const mapsUsageCollector = usageCollection.makeUsageCollector(collectorObj);
|
||||
usageCollection.registerCollector(mapsUsageCollector);
|
||||
}
|
||||
|
|
|
@ -4,60 +4,32 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import sinon from 'sinon';
|
||||
import { getMockCallWithInternal, getMockKbnServer, getMockTaskFetch } from '../test_utils';
|
||||
import { buildCollectorObj } from './maps_usage_collector';
|
||||
import { initTelemetryCollection } from './maps_usage_collector';
|
||||
|
||||
describe('buildCollectorObj#fetch', () => {
|
||||
let mockKbnServer;
|
||||
let makeUsageCollectorStub;
|
||||
let registerStub;
|
||||
let usageCollection;
|
||||
|
||||
beforeEach(() => {
|
||||
mockKbnServer = getMockKbnServer();
|
||||
makeUsageCollectorStub = jest.fn();
|
||||
registerStub = jest.fn();
|
||||
usageCollection = {
|
||||
makeUsageCollector: makeUsageCollectorStub,
|
||||
registerCollector: registerStub,
|
||||
};
|
||||
});
|
||||
|
||||
test('can return empty stats', async () => {
|
||||
const { type, fetch } = buildCollectorObj(mockKbnServer);
|
||||
expect(type).toBe('maps');
|
||||
const fetchResult = await fetch();
|
||||
expect(fetchResult).toEqual({});
|
||||
});
|
||||
test('makes and registers maps usage collector', async () => {
|
||||
const serverPlaceholder = {};
|
||||
initTelemetryCollection(usageCollection, serverPlaceholder);
|
||||
|
||||
test('provides known stats', async () => {
|
||||
const mockTaskFetch = getMockTaskFetch([
|
||||
{
|
||||
state: {
|
||||
runs: 2,
|
||||
stats: { wombat_sightings: { total: 712, max: 84, min: 7, avg: 63 } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch);
|
||||
|
||||
const { type, fetch } = buildCollectorObj(mockKbnServer);
|
||||
expect(type).toBe('maps');
|
||||
const fetchResult = await fetch();
|
||||
expect(fetchResult).toEqual({ wombat_sightings: { total: 712, max: 84, min: 7, avg: 63 } });
|
||||
});
|
||||
|
||||
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 } = buildCollectorObj(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('Sad violin'));
|
||||
mockKbnServer = getMockKbnServer(getMockCallWithInternal(), mockTaskFetch);
|
||||
|
||||
const { fetch } = buildCollectorObj(mockKbnServer);
|
||||
await expect(fetch()).rejects.toMatchObject(new Error('Sad violin'));
|
||||
expect(registerStub).toHaveBeenCalledTimes(1);
|
||||
expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1);
|
||||
expect(makeUsageCollectorStub).toHaveBeenCalledWith({
|
||||
type: expect.any(String),
|
||||
isReady: expect.any(Function),
|
||||
fetch: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* 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 { getMapsTelemetry } from './maps_telemetry';
|
||||
|
||||
const TELEMETRY_TASK_TYPE = 'maps_telemetry';
|
||||
|
||||
export const TASK_ID = `Maps-${TELEMETRY_TASK_TYPE}`;
|
||||
|
||||
export function scheduleTask(server) {
|
||||
const taskManager = server.plugins.task_manager;
|
||||
|
||||
if (!taskManager) {
|
||||
server.log(['debug', 'telemetry'], `Task manager is not available`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { kbnServer } = server.plugins.xpack_main.status.plugin;
|
||||
|
||||
kbnServer.afterPluginsInit(() => {
|
||||
// The code block below can't await directly within "afterPluginsInit"
|
||||
// callback due to circular dependency. The server isn't "ready" until
|
||||
// this code block finishes. Migrations wait for server to be ready before
|
||||
// executing. Saved objects repository waits for migrations to finish before
|
||||
// finishing the request. To avoid this, we'll await within a separate
|
||||
// function block.
|
||||
(async () => {
|
||||
try {
|
||||
await taskManager.ensureScheduled({
|
||||
id: TASK_ID,
|
||||
taskType: TELEMETRY_TASK_TYPE,
|
||||
state: { stats: {}, runs: 0 },
|
||||
});
|
||||
} catch (e) {
|
||||
server.log(['warning', 'maps'], `Error scheduling telemetry task, received ${e.message}`);
|
||||
}
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMapsTelemetryTask(server) {
|
||||
const taskManager = server.plugins.task_manager;
|
||||
|
||||
if (!taskManager) {
|
||||
server.log(['debug', 'telemetry'], `Task manager is not available`);
|
||||
return;
|
||||
}
|
||||
|
||||
taskManager.registerTaskDefinitions({
|
||||
[TELEMETRY_TASK_TYPE]: {
|
||||
title: 'Maps telemetry fetch task',
|
||||
type: TELEMETRY_TASK_TYPE,
|
||||
timeout: '1m',
|
||||
createTaskRunner: telemetryTaskRunner(server),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function telemetryTaskRunner(server) {
|
||||
return ({ taskInstance }) => {
|
||||
const { state } = taskInstance;
|
||||
const prevState = state;
|
||||
|
||||
const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser;
|
||||
|
||||
let mapsTelemetryTask;
|
||||
|
||||
return {
|
||||
async run({ taskCanceled = false } = {}) {
|
||||
try {
|
||||
mapsTelemetryTask = makeCancelable(getMapsTelemetry(server, callCluster), taskCanceled);
|
||||
} catch (err) {
|
||||
server.log(['warning'], `Error loading maps telemetry: ${err}`);
|
||||
} finally {
|
||||
return mapsTelemetryTask.promise
|
||||
.then((mapsTelemetry = {}) => {
|
||||
return {
|
||||
state: {
|
||||
runs: state.runs || 0 + 1,
|
||||
stats: mapsTelemetry.attributes || prevState.stats || {},
|
||||
},
|
||||
runAt: getNextMidnight(),
|
||||
};
|
||||
})
|
||||
.catch(errMsg =>
|
||||
server.log(['warning'], `Error executing maps telemetry task: ${errMsg}`)
|
||||
);
|
||||
}
|
||||
},
|
||||
async cancel() {
|
||||
if (mapsTelemetryTask) {
|
||||
mapsTelemetryTask.cancel();
|
||||
} else {
|
||||
server.log(['warning'], `Can not cancel "mapsTelemetryTask", it has not been defined`);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function makeCancelable(promise, isCanceled) {
|
||||
const logMsg = 'Maps telemetry task has been cancelled';
|
||||
const wrappedPromise = new Promise((resolve, reject) => {
|
||||
promise
|
||||
.then(val => (isCanceled ? reject(logMsg) : resolve(val)))
|
||||
.catch(err => (isCanceled ? reject(logMsg) : reject(err.message)));
|
||||
});
|
||||
|
||||
return {
|
||||
promise: wrappedPromise,
|
||||
cancel() {
|
||||
isCanceled = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getNextMidnight() {
|
||||
const nextMidnight = new Date();
|
||||
nextMidnight.setHours(0, 0, 0, 0);
|
||||
nextMidnight.setDate(nextMidnight.getDate() + 1);
|
||||
return nextMidnight;
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* 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 { getMockKbnServer, getMockTaskInstance } from '../test_utils';
|
||||
import { telemetryTaskRunner } from './telemetry_task';
|
||||
import * as mapsTelemetry from './maps_telemetry';
|
||||
jest.mock('./maps_telemetry');
|
||||
|
||||
const expectedAttributes = {
|
||||
expect: 'values',
|
||||
toBe: 'populated',
|
||||
};
|
||||
|
||||
const generateTelemetry = ({ includeAttributes = true } = {}) => {
|
||||
mapsTelemetry.getMapsTelemetry = async () => ({ // eslint-disable-line
|
||||
attributes: includeAttributes ? expectedAttributes : {},
|
||||
});
|
||||
};
|
||||
|
||||
describe('telemetryTaskRunner', () => {
|
||||
let mockTaskInstance;
|
||||
let mockKbnServer;
|
||||
let taskRunner;
|
||||
|
||||
beforeEach(() => {
|
||||
mockTaskInstance = getMockTaskInstance();
|
||||
mockKbnServer = getMockKbnServer();
|
||||
taskRunner = telemetryTaskRunner(mockKbnServer)({ taskInstance: mockTaskInstance });
|
||||
});
|
||||
|
||||
test('returns empty stats as default', async () => {
|
||||
generateTelemetry({ includeAttributes: false });
|
||||
|
||||
const runResult = await taskRunner.run();
|
||||
|
||||
expect(runResult).toMatchObject({
|
||||
state: {
|
||||
runs: 1,
|
||||
stats: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Return stats when run normally
|
||||
test('returns stats normally', async () => {
|
||||
generateTelemetry();
|
||||
|
||||
const runResult = await taskRunner.run();
|
||||
|
||||
expect(runResult).toMatchObject({
|
||||
state: {
|
||||
runs: 1,
|
||||
stats: expectedAttributes,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('cancels when cancel flag set to "true", returns undefined', async () => {
|
||||
generateTelemetry();
|
||||
|
||||
const runResult = await taskRunner.run({ taskCanceled: true });
|
||||
|
||||
expect(runResult).toBe(undefined);
|
||||
});
|
||||
});
|
|
@ -79,6 +79,10 @@ export default function({ getService }) {
|
|||
expect(stats.stack_stats.kibana.plugins.apm.services_per_agent).to.be.an('object');
|
||||
expect(stats.stack_stats.kibana.plugins.infraops.last_24_hours).to.be.an('object');
|
||||
expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string');
|
||||
expect(stats.stack_stats.kibana.plugins['maps-telemetry'].attributes.timeCaptured).to.be.a(
|
||||
'string'
|
||||
);
|
||||
|
||||
expect(stats.stack_stats.kibana.plugins.reporting.enabled).to.be(true);
|
||||
expect(stats.stack_stats.kibana.plugins.rollups.index_patterns).to.be.an('object');
|
||||
expect(stats.stack_stats.kibana.plugins.spaces.available).to.be(true);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue