[Telemetry] collector set to np (#51618)

* first iteration

* local collection ready

* type check

* fix collectorSet tests

* unskip test

* ordering

* collectors as array in constructor

* update README files

* update README and canvas to check for optional dep

* update README with more details

* Add file path for README example

* type UsageCollectionSetup

* run type check after refactor
This commit is contained in:
Ahmad Bamieh 2019-11-27 01:55:48 +02:00 committed by GitHub
parent 9d8c93158c
commit 0039e97747
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
112 changed files with 1046 additions and 868 deletions

1
.github/CODEOWNERS vendored
View file

@ -77,6 +77,7 @@
/src/dev/i18n @elastic/kibana-stack-services
/packages/kbn-analytics/ @elastic/kibana-stack-services
/src/legacy/core_plugins/ui_metric/ @elastic/kibana-stack-services
/src/plugins/usage_collection/ @elastic/kibana-stack-services
/x-pack/legacy/plugins/telemetry @elastic/kibana-stack-services
/x-pack/legacy/plugins/alerting @elastic/kibana-stack-services
/x-pack/legacy/plugins/actions @elastic/kibana-stack-services

View file

@ -210,3 +210,40 @@ export class Plugin {
}
}
```
### Usage Collection
For creating and registering a Usage Collector. Collectors would be defined in a separate directory `server/collectors/register.ts`. You can read more about usage collectors on `src/plugins/usage_collection/README.md`.
```ts
// server/collectors/register.ts
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void {
// usageCollection is an optional dependency, so make sure to return if it is not registered.
if (!usageCollection) {
return;
}
// create usage collector
const myCollector = usageCollection.makeUsageCollector({
type: MY_USAGE_TYPE,
fetch: async (callCluster: CallCluster) => {
// query ES and get some data
// summarize the data into a model
// return the modeled object that includes whatever you want to track
return {
my_objects: {
total: SOME_NUMBER
}
};
},
});
// register usage collector
usageCollection.registerCollector(myCollector);
}
```

View file

@ -324,6 +324,7 @@ export default function (kibana) {
},
init: async function (server) {
const { usageCollection } = server.newPlatform.setup.plugins;
// uuid
await manageUuid(server);
// routes
@ -338,8 +339,8 @@ export default function (kibana) {
registerKqlTelemetryApi(server);
registerFieldFormats(server);
registerTutorials(server);
makeKQLUsageCollector(server);
registerCspCollector(server);
makeKQLUsageCollector(usageCollection, server);
registerCspCollector(usageCollection, server);
server.expose('systemApi', systemApi);
server.injectUiAppVars('kibana', () => injectVars(server));
},

View file

@ -19,6 +19,7 @@
import { Server } from 'hapi';
import { createCSPRuleString, DEFAULT_CSP_RULES } from '../../../../../server/csp';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
export function createCspCollector(server: Server) {
return {
@ -42,8 +43,7 @@ export function createCspCollector(server: Server) {
};
}
export function registerCspCollector(server: Server): void {
const { collectorSet } = server.usage;
const collector = collectorSet.makeUsageCollector(createCspCollector(server));
collectorSet.register(collector);
export function registerCspCollector(usageCollection: UsageCollectionSetup, server: Server): void {
const collector = usageCollection.makeUsageCollector(createCspCollector(server));
usageCollection.registerCollector(collector);
}

View file

@ -19,14 +19,14 @@
import { fetchProvider } from './fetch';
export function makeKQLUsageCollector(server) {
export function makeKQLUsageCollector(usageCollection, server) {
const index = server.config().get('kibana.index');
const fetch = fetchProvider(index);
const kqlUsageCollector = server.usage.collectorSet.makeUsageCollector({
const kqlUsageCollector = usageCollection.makeUsageCollector({
type: 'kql',
fetch,
isReady: () => true,
});
server.usage.collectorSet.register(kqlUsageCollector);
usageCollection.registerCollector(kqlUsageCollector);
}

View file

@ -20,29 +20,30 @@
import { makeKQLUsageCollector } from './make_kql_usage_collector';
describe('makeKQLUsageCollector', () => {
let server;
let makeUsageCollectorStub;
let registerStub;
let usageCollection;
beforeEach(() => {
makeUsageCollectorStub = jest.fn();
registerStub = jest.fn();
usageCollection = {
makeUsageCollector: makeUsageCollectorStub,
registerCollector: registerStub,
};
server = {
usage: {
collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub },
},
config: () => ({ get: () => '.kibana' })
};
});
it('should call collectorSet.register', () => {
makeKQLUsageCollector(server);
it('should call registerCollector', () => {
makeKQLUsageCollector(usageCollection, server);
expect(registerStub).toHaveBeenCalledTimes(1);
});
it('should call makeUsageCollector with type = kql', () => {
makeKQLUsageCollector(server);
makeKQLUsageCollector(usageCollection, server);
expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1);
expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('kql');
});

View file

@ -0,0 +1,9 @@
# Kibana Telemetry Service
Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things:
1. Integrating with the telemetry service to express how to collect usage data (Collecting).
2. Sending a payload of usage data up to Elastic's telemetry cluster.
3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing).
This plugin is responsible for sending usage data to the telemetry cluster. For collecting usage data, use

View file

@ -27,14 +27,7 @@ import { i18n } from '@kbn/i18n';
import mappings from './mappings.json';
import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants';
import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated';
import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask } from './server';
import {
createLocalizationUsageCollector,
createTelemetryUsageCollector,
createUiMetricUsageCollector,
createTelemetryPluginUsageCollector,
} from './server/collectors';
import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask, PluginsSetup } from './server';
const ENDPOINT_VERSION = 'v2';
@ -123,6 +116,7 @@ const telemetry = (kibana: any) => {
fetcherTask.start();
},
init(server: Server) {
const { usageCollection } = server.newPlatform.setup.plugins;
const initializerContext = {
env: {
packageInfo: {
@ -149,12 +143,11 @@ const telemetry = (kibana: any) => {
log: server.log,
} as any) as CoreSetup;
telemetryPlugin(initializerContext).setup(coreSetup);
// register collectors
server.usage.collectorSet.register(createTelemetryPluginUsageCollector(server));
server.usage.collectorSet.register(createLocalizationUsageCollector(server));
server.usage.collectorSet.register(createTelemetryUsageCollector(server));
server.usage.collectorSet.register(createUiMetricUsageCollector(server));
const pluginsSetup: PluginsSetup = {
usageCollection,
};
telemetryPlugin(initializerContext).setup(coreSetup, pluginsSetup, server);
},
});
};

View file

@ -19,6 +19,7 @@
import { encryptTelemetry } from './collectors';
import { CallCluster } from '../../elasticsearch';
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server';
export type EncryptedStatsGetterConfig = { unencrypted: false } & {
server: any;
@ -37,6 +38,7 @@ export interface ClusterDetails {
}
export interface StatsCollectionConfig {
usageCollection: UsageCollectionSetup;
callCluster: CallCluster;
server: any;
start: string;
@ -112,7 +114,8 @@ export class TelemetryCollectionManager {
? (...args: any[]) => callWithRequest(config.req, ...args)
: callWithInternalUser;
return { server, callCluster, start, end };
const { usageCollection } = server.newPlatform.setup.plugins;
return { server, callCluster, start, end, usageCollection };
};
private getOptInStatsForCollection = async (

View file

@ -18,7 +18,7 @@
*/
export { encryptTelemetry } from './encryption';
export { createTelemetryUsageCollector } from './usage';
export { createUiMetricUsageCollector } from './ui_metric';
export { createLocalizationUsageCollector } from './localization';
export { createTelemetryPluginUsageCollector } from './telemetry_plugin';
export { registerTelemetryUsageCollector } from './usage';
export { registerUiMetricUsageCollector } from './ui_metric';
export { registerLocalizationUsageCollector } from './localization';
export { registerTelemetryPluginUsageCollector } from './telemetry_plugin';

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { createLocalizationUsageCollector } from './telemetry_localization_collector';
export { registerLocalizationUsageCollector } from './telemetry_localization_collector';

View file

@ -21,6 +21,7 @@ import { i18nLoader } from '@kbn/i18n';
import { size } from 'lodash';
import { getIntegrityHashes, Integrities } from './file_integrity';
import { KIBANA_LOCALIZATION_STATS_TYPE } from '../../../common/constants';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
export interface UsageStats {
locale: string;
integrities: Integrities;
@ -51,15 +52,15 @@ export function createCollectorFetch(server: any) {
};
}
/*
* @param {Object} server
* @return {Object} kibana usage stats type collection object
*/
export function createLocalizationUsageCollector(server: any) {
const { collectorSet } = server.usage;
return collectorSet.makeUsageCollector({
export function registerLocalizationUsageCollector(
usageCollection: UsageCollectionSetup,
server: any
) {
const collector = usageCollection.makeUsageCollector({
type: KIBANA_LOCALIZATION_STATS_TYPE,
isReady: () => true,
fetch: createCollectorFetch(server),
});
usageCollection.registerCollector(collector);
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { createTelemetryPluginUsageCollector } from './telemetry_plugin_collector';
export { registerTelemetryPluginUsageCollector } from './telemetry_plugin_collector';

View file

@ -20,6 +20,8 @@
import { TELEMETRY_STATS_TYPE } from '../../../common/constants';
import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository';
import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../telemetry_config';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
export interface TelemetryUsageStats {
opt_in_status?: boolean | null;
usage_fetcher?: 'browser' | 'server';
@ -61,15 +63,15 @@ export function createCollectorFetch(server: any) {
};
}
/*
* @param {Object} server
* @return {Object} kibana usage stats type collection object
*/
export function createTelemetryPluginUsageCollector(server: any) {
const { collectorSet } = server.usage;
return collectorSet.makeUsageCollector({
export function registerTelemetryPluginUsageCollector(
usageCollection: UsageCollectionSetup,
server: any
) {
const collector = usageCollection.makeUsageCollector({
type: TELEMETRY_STATS_TYPE,
isReady: () => true,
fetch: createCollectorFetch(server),
});
usageCollection.registerCollector(collector);
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { createUiMetricUsageCollector } from './telemetry_ui_metric_collector';
export { registerUiMetricUsageCollector } from './telemetry_ui_metric_collector';

View file

@ -18,10 +18,10 @@
*/
import { UI_METRIC_USAGE_TYPE } from '../../../common/constants';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
export function createUiMetricUsageCollector(server: any) {
const { collectorSet } = server.usage;
return collectorSet.makeUsageCollector({
export function registerUiMetricUsageCollector(usageCollection: UsageCollectionSetup, server: any) {
const collector = usageCollection.makeUsageCollector({
type: UI_METRIC_USAGE_TYPE,
fetch: async () => {
const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects;
@ -55,4 +55,6 @@ export function createUiMetricUsageCollector(server: any) {
},
isReady: () => true,
});
usageCollection.registerCollector(collector);
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { createTelemetryUsageCollector } from './telemetry_usage_collector';
export { registerTelemetryUsageCollector } from './telemetry_usage_collector';

View file

@ -25,20 +25,15 @@ import {
createTelemetryUsageCollector,
isFileReadable,
readTelemetryFile,
KibanaHapiServer,
MAX_FILE_SIZE,
} from './telemetry_usage_collector';
const getMockServer = (): KibanaHapiServer =>
({
usage: {
collectorSet: { makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg) },
},
} as KibanaHapiServer & Server);
const mockUsageCollector = () => ({
makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg),
});
const serverWithConfig = (configPath: string): KibanaHapiServer & Server => {
const serverWithConfig = (configPath: string): Server => {
return {
...getMockServer(),
config: () => ({
get: (key: string) => {
if (key !== 'telemetry.config' && key !== 'xpack.xpack_main.telemetry.config') {
@ -48,7 +43,7 @@ const serverWithConfig = (configPath: string): KibanaHapiServer & Server => {
return configPath;
},
}),
} as KibanaHapiServer & Server;
} as Server;
};
describe('telemetry_usage_collector', () => {
@ -130,14 +125,15 @@ describe('telemetry_usage_collector', () => {
});
describe('createTelemetryUsageCollector', () => {
test('calls `collectorSet.makeUsageCollector`', async () => {
test('calls `makeUsageCollector`', async () => {
// note: it uses the file's path to get the directory, then looks for 'telemetry.yml'
// exclusively, which is indirectly tested by passing it the wrong "file" in the same
// dir
const server: KibanaHapiServer & Server = serverWithConfig(tempFiles.unreadable);
const server: Server = serverWithConfig(tempFiles.unreadable);
// the `makeUsageCollector` is mocked above to return the argument passed to it
const collectorOptions = createTelemetryUsageCollector(server);
const usageCollector = mockUsageCollector() as any;
const collectorOptions = createTelemetryUsageCollector(usageCollector, server);
expect(collectorOptions.type).toBe('static_telemetry');
expect(await collectorOptions.fetch()).toEqual(expectedObject);

View file

@ -25,20 +25,13 @@ import { dirname, join } from 'path';
// look for telemetry.yml in the same places we expect kibana.yml
import { ensureDeepObject } from './ensure_deep_object';
import { getXpackConfigWithDeprecated } from '../../../common/get_xpack_config_with_deprecated';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
/**
* The maximum file size before we ignore it (note: this limit is arbitrary).
*/
export const MAX_FILE_SIZE = 10 * 1024; // 10 KB
export interface KibanaHapiServer extends Server {
usage: {
collectorSet: {
makeUsageCollector: (collector: object) => any;
};
};
}
/**
* Determine if the supplied `path` is readable.
*
@ -83,19 +76,11 @@ export async function readTelemetryFile(path: string): Promise<object | undefine
return undefined;
}
/**
* Create a usage collector that provides the `telemetry.yml` data as a
* `static_telemetry` object.
*
* Loading of the file is done lazily and on-demand. This avoids hanging
* onto the data in memory unnecessarily, as well as allows it to be
* updated out-of-process without having to restart Kibana.
*
* @param server The Kibana server instance.
* @return `UsageCollector` that provides the `static_telemetry` described.
*/
export function createTelemetryUsageCollector(server: KibanaHapiServer) {
return server.usage.collectorSet.makeUsageCollector({
export function createTelemetryUsageCollector(
usageCollection: UsageCollectionSetup,
server: Server
) {
return usageCollection.makeUsageCollector({
type: 'static_telemetry',
isReady: () => true,
fetch: async () => {
@ -106,3 +91,11 @@ export function createTelemetryUsageCollector(server: KibanaHapiServer) {
},
});
}
export function registerTelemetryUsageCollector(
usageCollection: UsageCollectionSetup,
server: Server
) {
const collector = createTelemetryUsageCollector(usageCollection, server);
usageCollection.registerCollector(collector);
}

View file

@ -24,7 +24,7 @@ import * as constants from '../common/constants';
export { FetcherTask } from './fetcher';
export { replaceTelemetryInjectedVars } from './telemetry_config';
export { telemetryCollectionManager } from './collection_manager';
export { PluginsSetup } from './plugin';
export const telemetryPlugin = (initializerContext: PluginInitializerContext) =>
new TelemetryPlugin(initializerContext);
export { constants };

View file

@ -18,8 +18,20 @@
*/
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { Server } from 'hapi';
import { registerRoutes } from './routes';
import { registerCollection } from './telemetry_collection';
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server';
import {
registerUiMetricUsageCollector,
registerTelemetryUsageCollector,
registerLocalizationUsageCollector,
registerTelemetryPluginUsageCollector,
} from './collectors';
export interface PluginsSetup {
usageCollection: UsageCollectionSetup;
}
export class TelemetryPlugin {
private readonly currentKibanaVersion: string;
@ -28,9 +40,15 @@ export class TelemetryPlugin {
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
}
public setup(core: CoreSetup) {
public setup(core: CoreSetup, { usageCollection }: PluginsSetup, server: Server) {
const currentKibanaVersion = this.currentKibanaVersion;
registerCollection();
registerRoutes({ core, currentKibanaVersion });
registerTelemetryPluginUsageCollector(usageCollection, server);
registerLocalizationUsageCollector(usageCollection, server);
registerTelemetryUsageCollector(usageCollection, server);
registerUiMetricUsageCollector(usageCollection, server);
}
}

View file

@ -29,7 +29,12 @@ import {
handleLocalStats,
} from '../get_local_stats';
const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({
const mockUsageCollection = (kibanaUsage = {}) => ({
bulkFetch: () => kibanaUsage,
toObject: data => data,
});
const getMockServer = (getCluster = sinon.stub()) => ({
log(tags, message) {
console.log({ tags, message });
},
@ -43,7 +48,6 @@ const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({
}
};
},
usage: { collectorSet: { bulkFetch: () => kibanaUsage, toObject: data => data } },
plugins: {
elasticsearch: { getCluster },
},
@ -155,15 +159,16 @@ describe('get_local_stats', () => {
describe.skip('getLocalStats', () => {
it('returns expected object without xpack data when X-Pack fails to respond', async () => {
const callClusterUsageFailed = sinon.stub();
const usageCollection = mockUsageCollection();
mockGetLocalStats(
callClusterUsageFailed,
Promise.resolve(clusterInfo),
Promise.resolve(clusterStats),
);
const result = await getLocalStats({
const result = await getLocalStats([], {
server: getMockServer(),
callCluster: callClusterUsageFailed,
usageCollection,
});
expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid);
expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name);
@ -178,15 +183,16 @@ describe('get_local_stats', () => {
it('returns expected object with xpack and kibana data', async () => {
const callCluster = sinon.stub();
const usageCollection = mockUsageCollection(kibana);
mockGetLocalStats(
callCluster,
Promise.resolve(clusterInfo),
Promise.resolve(clusterStats),
);
const result = await getLocalStats({
server: getMockServer(callCluster, kibana),
const result = await getLocalStats([], {
server: getMockServer(callCluster),
usageCollection,
callCluster,
});

View file

@ -47,12 +47,7 @@ export function handleKibanaStats(server, response) {
};
}
/*
* Check user privileges for read access to monitoring
* Pass callWithInternalUser to bulkFetchUsage
*/
export async function getKibana(server, callWithInternalUser) {
const { collectorSet } = server.usage;
const usage = await collectorSet.bulkFetch(callWithInternalUser);
return collectorSet.toObject(usage);
export async function getKibana(usageCollection, callWithInternalUser) {
const usage = await usageCollection.bulkFetch(callWithInternalUser);
return usageCollection.toObject(usage);
}

View file

@ -55,13 +55,14 @@ export function handleLocalStats(server: any, clusterInfo: any, clusterStats: an
* @return {Promise} The object containing the current Elasticsearch cluster's telemetry.
*/
export const getLocalStats: StatsGetter = async (clustersDetails, config) => {
const { server, callCluster } = config;
const { server, callCluster, usageCollection } = config;
return await Promise.all(
clustersDetails.map(async clustersDetail => {
const [clusterInfo, clusterStats, kibana] = await Promise.all([
getClusterInfo(callCluster), // cluster info
getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_)
getKibana(server, callCluster),
getKibana(usageCollection, callCluster),
]);
return handleLocalStats(server, clusterInfo, clusterStats, kibana);
})

View file

@ -38,7 +38,7 @@ import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from '../../core/serve
import { SavedObjectsManagement } from '../../core/server/saved_objects/management';
import { ApmOssPlugin } from '../core_plugins/apm_oss';
import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch';
import { UsageCollectionSetup } from '../../plugins/usage_collection/server';
import { CapabilitiesModifier } from './capabilities';
import { IndexPatternsServiceFactory } from './index_patterns';
import { Capabilities } from '../../core/public';
@ -67,7 +67,6 @@ declare module 'hapi' {
config: () => KibanaConfig;
indexPatternsServiceFactory: IndexPatternsServiceFactory;
savedObjects: SavedObjectsLegacyService;
usage: { collectorSet: any };
injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void;
getHiddenUiAppById(appId: string): UiApp;
registerCapabilitiesModifier: (provider: CapabilitiesModifier) => void;
@ -101,6 +100,11 @@ declare module 'hapi' {
type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise<any> | void;
export interface PluginsSetup {
usageCollection: UsageCollectionSetup;
[key: string]: object;
}
// eslint-disable-next-line import/no-default-export
export default class KbnServer {
public readonly newPlatform: {
@ -120,7 +124,7 @@ export default class KbnServer {
};
setup: {
core: CoreSetup;
plugins: Record<string, object>;
plugins: PluginsSetup;
};
start: {
core: CoreSetup;

View file

@ -28,7 +28,6 @@ import httpMixin from './http';
import { coreMixin } from './core';
import { loggingMixin } from './logging';
import warningsMixin from './warnings';
import { usageMixin } from './usage';
import { statusMixin } from './status';
import pidMixin from './pid';
import { configDeprecationWarningsMixin } from './config/deprecation_warnings';
@ -94,7 +93,6 @@ export default class KbnServer {
loggingMixin,
configDeprecationWarningsMixin,
warningsMixin,
usageMixin,
statusMixin,
// writes pid file

View file

@ -17,26 +17,25 @@
* under the License.
*/
import * as Hapi from 'hapi';
import { Server } from 'hapi';
import { fetchProvider } from './collector_fetch';
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server';
interface KbnServer extends Hapi.Server {
usage: any;
}
export function makeSampleDataUsageCollector(server: KbnServer) {
export function makeSampleDataUsageCollector(
usageCollection: UsageCollectionSetup,
server: Server
) {
let index: string;
try {
index = server.config().get('kibana.index');
} catch (err) {
return; // kibana plugin is not enabled (test environment)
}
const collector = usageCollection.makeUsageCollector({
type: 'sample-data',
fetch: fetchProvider(index),
isReady: () => true,
});
server.usage.collectorSet.register(
server.usage.collectorSet.makeUsageCollector({
type: 'sample-data',
fetch: fetchProvider(index),
isReady: () => true,
})
);
usageCollection.registerCollector(collector);
}

View file

@ -35,9 +35,8 @@ import { getKibanaInfoForStats } from '../lib';
* the metrics.
* See PR comment in https://github.com/elastic/kibana/pull/20577/files#r202416647
*/
export function getOpsStatsCollector(server, kbnServer) {
const { collectorSet } = server.usage;
return collectorSet.makeStatsCollector({
export function getOpsStatsCollector(usageCollection, server, kbnServer) {
return usageCollection.makeStatsCollector({
type: KIBANA_STATS_TYPE,
fetch: () => {
return {
@ -49,3 +48,10 @@ export function getOpsStatsCollector(server, kbnServer) {
ignoreForInternalUploader: true, // Ignore this one from internal uploader. A different stats collector is used there.
});
}
export function registerOpsStatsCollector(usageCollection, server, kbnServer) {
if (usageCollection) {
const collector = getOpsStatsCollector(usageCollection, server, kbnServer);
usageCollection.registerCollector(collector);
}
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { getOpsStatsCollector } from './get_ops_stats_collector';
export { registerOpsStatsCollector } from './get_ops_stats_collector';

View file

@ -20,17 +20,15 @@
import ServerStatus from './server_status';
import { Metrics } from './lib/metrics';
import { registerStatusPage, registerStatusApi, registerStatsApi } from './routes';
import { getOpsStatsCollector } from './collectors';
import { registerOpsStatsCollector } from './collectors';
import Oppsy from 'oppsy';
import { cloneDeep } from 'lodash';
import { getOSInfo } from './lib/get_os_info';
export function statusMixin(kbnServer, server, config) {
kbnServer.status = new ServerStatus(kbnServer.server);
const statsCollector = getOpsStatsCollector(server, kbnServer);
const { collectorSet } = server.usage;
collectorSet.register(statsCollector);
const { usageCollection } = server.newPlatform.setup.plugins;
registerOpsStatsCollector(usageCollection, server, kbnServer);
const metrics = new Metrics(config, server);
@ -57,7 +55,7 @@ export function statusMixin(kbnServer, server, config) {
// init routes
registerStatusPage(kbnServer, server, config);
registerStatusApi(kbnServer, server, config);
registerStatsApi(kbnServer, server, config);
registerStatsApi(usageCollection, server, config);
// expore shared functionality
server.decorate('server', 'getOSInfo', getOSInfo);

View file

@ -29,7 +29,7 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', {
/*
* API for Kibana meta info and accumulated operations stats
* Including ?extended in the query string fetches Elasticsearch cluster_uuid and server.usage.collectorSet data
* Including ?extended in the query string fetches Elasticsearch cluster_uuid and usageCollection data
* - Requests to set isExtended = true
* GET /api/stats?extended=true
* GET /api/stats?extended
@ -37,9 +37,8 @@ const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', {
* - Any other value causes a statusCode 400 response (Bad Request)
* Including ?exclude_usage in the query string excludes the usage stats from the response. Same value semantics as ?extended
*/
export function registerStatsApi(kbnServer, server, config) {
export function registerStatsApi(usageCollection, server, config) {
const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous'));
const { collectorSet } = server.usage;
const getClusterUuid = async callCluster => {
const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid', });
@ -47,8 +46,8 @@ export function registerStatsApi(kbnServer, server, config) {
};
const getUsage = async callCluster => {
const usage = await collectorSet.bulkFetchUsage(callCluster);
return collectorSet.toObject(usage);
const usage = await usageCollection.bulkFetchUsage(callCluster);
return usageCollection.toObject(usage);
};
server.route(
@ -74,7 +73,7 @@ export function registerStatsApi(kbnServer, server, config) {
if (isExtended) {
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin');
const callCluster = (...args) => callWithRequest(req, ...args);
const collectorsReady = await collectorSet.areAllCollectorsReady();
const collectorsReady = await usageCollection.areAllCollectorsReady();
if (shouldGetUsage && !collectorsReady) {
return boom.serverUnavailable(STATS_NOT_READY_MESSAGE);
@ -126,7 +125,7 @@ export function registerStatsApi(kbnServer, server, config) {
};
}
else {
extended = collectorSet.toApiFieldNames({
extended = usageCollection.toApiFieldNames({
usage: modifiedUsage,
clusterUuid
});
@ -139,12 +138,12 @@ export function registerStatsApi(kbnServer, server, config) {
/* kibana_stats gets singled out from the collector set as it is used
* for health-checking Kibana and fetch does not rely on fetching data
* from ES */
const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE);
const kibanaStatsCollector = usageCollection.getCollectorByType(KIBANA_STATS_TYPE);
if (!await kibanaStatsCollector.isReady()) {
return boom.serverUnavailable(STATS_NOT_READY_MESSAGE);
}
let kibanaStats = await kibanaStatsCollector.fetch();
kibanaStats = collectorSet.toApiFieldNames(kibanaStats);
kibanaStats = usageCollection.toApiFieldNames(kibanaStats);
return {
...kibanaStats,

View file

@ -1,92 +0,0 @@
# Kibana Telemetry Service
Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things:
1. Integrating with the telemetry service to express how to collect usage data (Collecting).
2. Sending a payload of usage data up to Elastic's telemetry cluster, once per browser per day (Sending).
3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing).
You, the feature or plugin developer, mainly need to worry about the first meaning: collecting. To integrate with the telemetry services for usage collection of your feature, there are 2 steps:
1. Create a usage collector using a factory function
2. Register the usage collector with the Telemetry service
NOTE: To a lesser extent, there's also a need to update the telemetry payload of Kibana stats and telemetry cluster field mappings to include your fields. This part is typically handled not by you, the developer, but different maintainers of the telemetry cluster. Usually, this step just means talk to the Platform team and have them approve your data model or added fields.
## Creating and Registering Usage Collector
A usage collector object is an instance of a class called `UsageCollector`. A factory function on `server.usage.collectorSet` object allows you to create an instance of this class. All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it.
Example:
```js
// create usage collector
const myCollector = server.usage.collectorSet.makeUsageCollector({
type: MY_USAGE_TYPE,
fetch: async callCluster => {
// query ES and get some data
// summarize the data into a model
// return the modeled object that includes whatever you want to track
return {
my_objects: {
total: SOME_NUMBER
}
};
},
});
// register usage collector
server.usage.collectorSet.register(myCollector);
```
Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection.
The fetch method also might be called through an internal background task on the Kibana server, which currently lives in the `kibana_monitoring` module of the X-Pack Monitoring plugin, that polls for data and uploads it to Elasticsearch through a bulk API exposed by the Monitoring plugin for Elasticsearch. In this case, the `callCluster` method will be the internal system user and will have read privilege over the entire `.kibana` index.
Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS.
Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`.
## Update the telemetry payload and telemetry cluster field mappings
There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster.
As of the time of this writing (pre-6.5.0) there are a few unpleasant realities with this module. Today, this module has to be aware of all the features that have integrated with it, which it does from hard-coding. It does this because at the time of creation, the payload implemented a designed model where X-Pack plugin info went together regardless if it was ES-specific or Kibana-specific. In hindsight, all the Kibana data could just be put together, X-Pack or not, which it could do in a generic way. This is a known problem and a solution will be implemented in an upcoming refactoring phase, as this would break the contract for model of data sent in the payload.
The second reality is that new fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in Kibana visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster.
## Testing
There are a few ways you can test that your usage collector is working properly.
1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`.
2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from:
- The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes.
- Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data. ✳
- The dev script in x-pack can be run on the command-line with:
```
cd x-pack
node scripts/api_debug.js telemetry --host=http://localhost:5601
```
Where `http://localhost:5601` is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well.
- Automatic inclusion of all the stats fetched by collectors is added in https://github.com/elastic/kibana/pull/22336 / 6.5.0
3. In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields.
4. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload.
✳ At the time of this writing, there is an open issue that in the sending phase, Kibana usage collectors are not "live-pulled" from Kibana API endpoints if Monitoring is disabled. The implementation on this depends on a new secure way to live-pull the data from the end-user's browser, as it would not be appropriate to supply only partial data if the logged-in user only has partial access to `.kibana`.
## FAQ
1. **Can telemetry track UI interactions, such as button click?**
Brief answer: no. Telemetry collection happens on the server-side so the usage data will only include information that the server-side is aware of. There is no generic way to do this today, but UI-interaction KPIs can be tracked with a custom server endpoint that gets called for tracking when the UI event happens.
2. **Does the telemetry service have a hook that I can call whenever some event happens in my feature?**
Brief answer: no. Telemetry collection is a fetch model, not a push model. Telemetry fetches info from your collector.
3. **How should I design my data model?**
Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine.
4. **Can the telemetry payload include dynamic fields?**
Yes. When you talk to the Platform team about new fields being added, point out specifically which properties will have dynamic inner fields.
5. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?**
Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that.

View file

@ -1,206 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { snakeCase } from 'lodash';
import { getCollectorLogger } from '../lib';
import { Collector } from './collector';
import { UsageCollector } from './usage_collector';
let _waitingForAllCollectorsTimestamp = null;
/*
* A collector object has types registered into it with the register(type)
* function. Each type that gets registered defines how to fetch its own data
* and optionally, how to combine it into a unified payload for bulk upload.
*/
export class CollectorSet {
/*
* @param {Object} server - server object
* @param {Array} collectors to initialize, usually as a result of filtering another CollectorSet instance
*/
constructor(server, collectors = [], config = null) {
this._log = getCollectorLogger(server);
this._collectors = collectors;
/*
* Helper Factory methods
* Define as instance properties to allow enclosing the server object
*/
this.makeStatsCollector = options => new Collector(server, options);
this.makeUsageCollector = options => new UsageCollector(server, options);
this._makeCollectorSetFromArray = collectorsArray => new CollectorSet(server, collectorsArray, config);
this._maximumWaitTimeForAllCollectorsInS = config ? config.get('stats.maximumWaitTimeForAllCollectorsInS') : 60;
}
/*
* @param collector {Collector} collector object
*/
register(collector) {
// check instanceof
if (!(collector instanceof Collector)) {
throw new Error('CollectorSet can only have Collector instances registered');
}
this._collectors.push(collector);
if (collector.init) {
this._log.debug(`Initializing ${collector.type} collector`);
collector.init();
}
}
getCollectorByType(type) {
return this._collectors.find(c => c.type === type);
}
// isUsageCollector(x: UsageCollector | any): x is UsageCollector {
isUsageCollector(x) {
return x instanceof UsageCollector;
}
async areAllCollectorsReady(collectorSet = this) {
if (!(collectorSet instanceof CollectorSet)) {
throw new Error(`areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet);
}
const collectorTypesNotReady = [];
let allReady = true;
await collectorSet.asyncEach(async collector => {
if (!await collector.isReady()) {
allReady = false;
collectorTypesNotReady.push(collector.type);
}
});
if (!allReady && this._maximumWaitTimeForAllCollectorsInS >= 0) {
const nowTimestamp = +new Date();
_waitingForAllCollectorsTimestamp = _waitingForAllCollectorsTimestamp || nowTimestamp;
const timeWaitedInMS = nowTimestamp - _waitingForAllCollectorsTimestamp;
const timeLeftInMS = (this._maximumWaitTimeForAllCollectorsInS * 1000) - timeWaitedInMS;
if (timeLeftInMS <= 0) {
this._log.debug(`All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) `
+ `but we have waited the required `
+ `${this._maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.`);
return true;
} else {
this._log.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`);
}
} else {
_waitingForAllCollectorsTimestamp = null;
}
return allReady;
}
/*
* Call a bunch of fetch methods and then do them in bulk
* @param {CollectorSet} collectorSet - a set of collectors to fetch. Default to all registered collectors
*/
async bulkFetch(callCluster, collectorSet = this) {
if (!(collectorSet instanceof CollectorSet)) {
throw new Error(`bulkFetch method given bad collectorSet parameter: ` + typeof collectorSet);
}
const responses = [];
await collectorSet.asyncEach(async collector => {
this._log.debug(`Fetching data from ${collector.type} collector`);
try {
responses.push({
type: collector.type,
result: await collector.fetchInternal(callCluster)
});
}
catch (err) {
this._log.warn(err);
this._log.warn(`Unable to fetch data from ${collector.type} collector`);
}
});
return responses;
}
/*
* @return {new CollectorSet}
*/
getFilteredCollectorSet(filter) {
const filtered = this._collectors.filter(filter);
return this._makeCollectorSetFromArray(filtered);
}
async bulkFetchUsage(callCluster) {
const usageCollectors = this.getFilteredCollectorSet(c => c instanceof UsageCollector);
return this.bulkFetch(callCluster, usageCollectors);
}
// convert an array of fetched stats results into key/object
toObject(statsData) {
if (!statsData) return {};
return statsData.reduce((accumulatedStats, { type, result }) => {
return {
...accumulatedStats,
[type]: result,
};
}, {});
}
// rename fields to use api conventions
toApiFieldNames(apiData) {
const getValueOrRecurse = value => {
if (value == null || typeof value !== 'object') {
return value;
} else {
return this.toApiFieldNames(value); // recurse
}
};
// handle array and return early, or return a reduced object
if (Array.isArray(apiData)) {
return apiData.map(getValueOrRecurse);
}
return Object.keys(apiData).reduce((accum, field) => {
const value = apiData[field];
let newName = field;
newName = snakeCase(newName);
newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m
newName = newName.replace('_in_bytes', '_bytes');
newName = newName.replace('_in_millis', '_ms');
return {
...accum,
[newName]: getValueOrRecurse(value),
};
}, {});
}
map(mapFn) {
return this._collectors.map(mapFn);
}
some(someFn) {
return this._collectors.some(someFn);
}
async asyncEach(eachFn) {
for (const collector of this._collectors) {
await eachFn(collector);
}
}
}

View file

@ -0,0 +1,139 @@
# Kibana Usage Collection Service
Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring).
To integrate with the telemetry services for usage collection of your feature, there are 2 steps:
1. Create a usage collector.
2. Register the usage collector.
## Creating and Registering Usage Collector
All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it.
### New Platform:
1. Make sure `usageCollection` is in your optional Plugins:
```json
// plugin/kibana.json
{
"id": "...",
"optionalPlugins": ["usageCollection"]
}
```
2. Register Usage collector in the `setup` function:
```ts
// server/plugin.ts
class Plugin {
setup(core, plugins) {
registerMyPluginUsageCollector(plugins.usageCollection);
}
}
```
3. Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory `server/collectors/register.ts`.
```ts
// server/collectors/register.ts
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void {
// usageCollection is an optional dependency, so make sure to return if it is not registered.
if (!usageCollection) {
return;
}
// create usage collector
const myCollector = usageCollection.makeUsageCollector({
type: MY_USAGE_TYPE,
fetch: async (callCluster: CallCluster) => {
// query ES and get some data
// summarize the data into a model
// return the modeled object that includes whatever you want to track
return {
my_objects: {
total: SOME_NUMBER
}
};
},
});
// register usage collector
usageCollection.registerCollector(myCollector);
}
```
Some background: The `callCluster` that gets passed to the `fetch` method is created in a way that's a bit tricky, to support multiple contexts the `fetch` method could be called. Your `fetch` method could get called as a result of an HTTP API request: in this case, the `callCluster` function wraps `callWithRequest`, and the request headers are expected to have read privilege on the entire `.kibana` index. The use case for this is stats pulled from a Kibana Metricbeat module, where the Beat calls Kibana's stats API in Kibana to invoke collection.
Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS.
### Migrating to NP from Legacy Plugins:
Pass `usageCollection` to the setup NP plugin setup function under plugins. Inside the `setup` function call the `registerCollector` like what you'd do in the NP example above.
```js
// index.js
export const myPlugin = (kibana: any) => {
return new kibana.Plugin({
init: async function (server) {
const { usageCollection } = server.newPlatform.setup.plugins;
const plugins = {
usageCollection,
};
plugin(initializerContext).setup(core, plugins);
}
});
}
```
### Legacy Plugins:
Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`.
```js
// index.js
export const myPlugin = (kibana: any) => {
return new kibana.Plugin({
init: async function (server) {
const { usageCollection } = server.newPlatform.setup.plugins;
registerMyPluginUsageCollector(usageCollection);
}
});
}
```
## Update the telemetry payload and telemetry cluster field mappings
There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster.
New fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in Kibana visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster.
## Testing
There are a few ways you can test that your usage collector is working properly.
1. The `/api/stats?extended=true` HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the `usage` object of the response named after your usage collector's `type` field. This method tests the Metricbeat scenario described above where `callCluster` wraps `callWithRequest`.
2. There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from:
- The `.monitoring-*` indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes.
- Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data.
- The dev script in x-pack can be run on the command-line with:
```
cd x-pack
node scripts/api_debug.js telemetry --host=http://localhost:5601
```
Where `http://localhost:5601` is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well.
- Automatic inclusion of all the stats fetched by collectors is added in https://github.com/elastic/kibana/pull/22336 / 6.5.0
3. In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields.
4. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload.
## FAQ
1. **How should I design my data model?**
Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine.
2. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?**
Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that.

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { getCollectorLogger } from './get_collector_logger';
export const KIBANA_STATS_TYPE = 'kibana_stats';

View file

@ -0,0 +1,7 @@
{
"id": "usageCollection",
"configPath": ["usageCollection"],
"version": "kibana",
"server": true,
"ui": false
}

View file

@ -24,22 +24,25 @@ import { Collector } from '../collector';
import { CollectorSet } from '../collector_set';
import { UsageCollector } from '../usage_collector';
const mockLogger = () => ({
debug: sinon.spy(),
warn: sinon.spy(),
});
describe('CollectorSet', () => {
describe('registers a collector set and runs lifecycle events', () => {
let server;
let init;
let fetch;
beforeEach(() => {
server = { log: sinon.spy() };
init = noop;
fetch = noop;
});
it('should throw an error if non-Collector type of object is registered', () => {
const collectors = new CollectorSet(server);
const logger = mockLogger();
const collectors = new CollectorSet({ logger });
const registerPojo = () => {
collectors.register({
collectors.registerCollector({
type: 'type_collector_test',
init,
fetch,
@ -53,17 +56,17 @@ describe('CollectorSet', () => {
it('should log debug status of fetching from the collector', async () => {
const mockCallCluster = () => Promise.resolve({ passTest: 1000 });
const collectors = new CollectorSet(server);
collectors.register(new Collector(server, {
const logger = mockLogger();
const collectors = new CollectorSet({ logger });
collectors.registerCollector(new Collector(logger, {
type: 'MY_TEST_COLLECTOR',
fetch: caller => caller()
}));
const result = await collectors.bulkFetch(mockCallCluster);
const calls = server.log.getCalls();
const calls = logger.debug.getCalls();
expect(calls.length).to.be(1);
expect(calls[0].args).to.eql([
['debug', 'stats-collection'],
'Fetching data from MY_TEST_COLLECTOR collector',
]);
expect(result).to.eql([{
@ -74,8 +77,9 @@ describe('CollectorSet', () => {
it('should gracefully handle a collector fetch method throwing an error', async () => {
const mockCallCluster = () => Promise.resolve({ passTest: 1000 });
const collectors = new CollectorSet(server);
collectors.register(new Collector(server, {
const logger = mockLogger();
const collectors = new CollectorSet({ logger });
collectors.registerCollector(new Collector(logger, {
type: 'MY_TEST_COLLECTOR',
fetch: () => new Promise((_resolve, reject) => reject())
}));
@ -95,7 +99,8 @@ describe('CollectorSet', () => {
let collectorSet;
beforeEach(() => {
collectorSet = new CollectorSet();
const logger = mockLogger();
collectorSet = new CollectorSet({ logger });
});
it('should snake_case and convert field names to api standards', () => {
@ -161,14 +166,13 @@ describe('CollectorSet', () => {
});
describe('isUsageCollector', () => {
const server = { };
const collectorOptions = { type: 'MY_TEST_COLLECTOR', fetch: () => {} };
it('returns true only for UsageCollector instances', () => {
const collectors = new CollectorSet(server);
const usageCollector = new UsageCollector(server, collectorOptions);
const collector = new Collector(server, collectorOptions);
const logger = mockLogger();
const collectors = new CollectorSet({ logger });
const usageCollector = new UsageCollector(logger, collectorOptions);
const collector = new Collector(logger, collectorOptions);
const randomClass = new (class Random {});
expect(collectors.isUsageCollector(usageCollector)).to.be(true);
expect(collectors.isUsageCollector(collector)).to.be(false);

View file

@ -17,18 +17,17 @@
* under the License.
*/
import { getCollectorLogger } from '../lib';
export class Collector {
/*
* @param {Object} server - server object
* @param {Object} logger - logger object
* @param {String} options.type - property name as the key for the data
* @param {Function} options.init (optional) - initialization function
* @param {Function} options.fetch - function to query data
* @param {Function} options.formatForBulkUpload - optional
* @param {Function} options.rest - optional other properties
*/
constructor(server, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) {
constructor(logger, { type, init, fetch, formatForBulkUpload = null, isReady = null, ...options } = {}) {
if (type === undefined) {
throw new Error('Collector must be instantiated with a options.type string property');
}
@ -39,7 +38,7 @@ export class Collector {
throw new Error('Collector must be instantiated with a options.fetch function property');
}
this.log = getCollectorLogger(server);
this.log = logger;
Object.assign(this, options); // spread in other properties and mutate "this"

View file

@ -0,0 +1,209 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { snakeCase } from 'lodash';
import { Logger } from 'kibana/server';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
// @ts-ignore
import { Collector } from './collector';
// @ts-ignore
import { UsageCollector } from './usage_collector';
interface CollectorSetConfig {
logger: Logger;
maximumWaitTimeForAllCollectorsInS: number;
collectors?: Collector[];
}
export class CollectorSet {
private _waitingForAllCollectorsTimestamp?: number;
private logger: Logger;
private readonly maximumWaitTimeForAllCollectorsInS: number;
private collectors: Collector[] = [];
constructor({ logger, maximumWaitTimeForAllCollectorsInS, collectors = [] }: CollectorSetConfig) {
this.logger = logger;
this.collectors = collectors;
this.maximumWaitTimeForAllCollectorsInS = maximumWaitTimeForAllCollectorsInS || 60;
}
public makeStatsCollector = (options: any) => {
return new Collector(this.logger, options);
};
public makeUsageCollector = (options: any) => {
return new UsageCollector(this.logger, options);
};
/*
* @param collector {Collector} collector object
*/
public registerCollector = (collector: Collector) => {
// check instanceof
if (!(collector instanceof Collector)) {
throw new Error('CollectorSet can only have Collector instances registered');
}
this.collectors.push(collector);
if (collector.init) {
this.logger.debug(`Initializing ${collector.type} collector`);
collector.init();
}
};
public getCollectorByType = (type: string) => {
return this.collectors.find(c => c.type === type);
};
public isUsageCollector = (x: UsageCollector | any): x is UsageCollector => {
return x instanceof UsageCollector;
};
public areAllCollectorsReady = async (collectorSet = this) => {
if (!(collectorSet instanceof CollectorSet)) {
throw new Error(
`areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet
);
}
const collectorTypesNotReady: string[] = [];
let allReady = true;
for (const collector of collectorSet.collectors) {
if (!(await collector.isReady())) {
allReady = false;
collectorTypesNotReady.push(collector.type);
}
}
if (!allReady && this.maximumWaitTimeForAllCollectorsInS >= 0) {
const nowTimestamp = +new Date();
this._waitingForAllCollectorsTimestamp =
this._waitingForAllCollectorsTimestamp || nowTimestamp;
const timeWaitedInMS = nowTimestamp - this._waitingForAllCollectorsTimestamp;
const timeLeftInMS = this.maximumWaitTimeForAllCollectorsInS * 1000 - timeWaitedInMS;
if (timeLeftInMS <= 0) {
this.logger.debug(
`All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` +
`but we have waited the required ` +
`${this.maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.`
);
return true;
} else {
this.logger.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`);
}
} else {
this._waitingForAllCollectorsTimestamp = undefined;
}
return allReady;
};
public bulkFetch = async (
callCluster: CallCluster,
collectors: Collector[] = this.collectors
) => {
const responses = [];
for (const collector of collectors) {
this.logger.debug(`Fetching data from ${collector.type} collector`);
try {
responses.push({
type: collector.type,
result: await collector.fetchInternal(callCluster),
});
} catch (err) {
this.logger.warn(err);
this.logger.warn(`Unable to fetch data from ${collector.type} collector`);
}
}
return responses;
};
/*
* @return {new CollectorSet}
*/
public getFilteredCollectorSet = (filter: any) => {
const filtered = this.collectors.filter(filter);
return this.makeCollectorSetFromArray(filtered);
};
public bulkFetchUsage = async (callCluster: CallCluster) => {
const usageCollectors = this.getFilteredCollectorSet((c: any) => c instanceof UsageCollector);
return await this.bulkFetch(callCluster, usageCollectors.collectors);
};
// convert an array of fetched stats results into key/object
public toObject = (statsData: any) => {
if (!statsData) return {};
return statsData.reduce((accumulatedStats: any, { type, result }: any) => {
return {
...accumulatedStats,
[type]: result,
};
}, {});
};
// rename fields to use api conventions
public toApiFieldNames = (apiData: any): any => {
const getValueOrRecurse = (value: any) => {
if (value == null || typeof value !== 'object') {
return value;
} else {
return this.toApiFieldNames(value); // recurse
}
};
// handle array and return early, or return a reduced object
if (Array.isArray(apiData)) {
return apiData.map(getValueOrRecurse);
}
return Object.keys(apiData).reduce((accum, field) => {
const value = apiData[field];
let newName = field;
newName = snakeCase(newName);
newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m
newName = newName.replace('_in_bytes', '_bytes');
newName = newName.replace('_in_millis', '_ms');
return {
...accum,
[newName]: getValueOrRecurse(value),
};
}, {});
};
// TODO: remove
public map = (mapFn: any) => {
return this.collectors.map(mapFn);
};
// TODO: remove
public some = (someFn: any) => {
return this.collectors.some(someFn);
};
private makeCollectorSetFromArray = (collectors: Collector[]) => {
return new CollectorSet({
logger: this.logger,
maximumWaitTimeForAllCollectorsInS: this.maximumWaitTimeForAllCollectorsInS,
collectors,
});
};
}

View file

@ -18,5 +18,7 @@
*/
export { CollectorSet } from './collector_set';
// @ts-ignore
export { Collector } from './collector';
// @ts-ignore
export { UsageCollector } from './usage_collector';

View file

@ -17,20 +17,20 @@
* under the License.
*/
import { KIBANA_STATS_TYPE } from '../../status/constants';
import { KIBANA_STATS_TYPE } from '../../common/constants';
import { Collector } from './collector';
export class UsageCollector extends Collector {
/*
* @param {Object} server - server object
* @param {Object} logger - logger object
* @param {String} options.type - property name as the key for the data
* @param {Function} options.init (optional) - initialization function
* @param {Function} options.fetch - function to query data
* @param {Function} options.formatForBulkUpload - optional
* @param {Function} options.rest - optional other properties
*/
constructor(server, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) {
super(server, { type, init, fetch, formatForBulkUpload, ...options });
constructor(logger, { type, init, fetch, formatForBulkUpload = null, ...options } = {}) {
super(logger, { type, init, fetch, formatForBulkUpload, ...options });
/*
* Currently, for internal bulk uploading, usage stats are part of

View file

@ -17,15 +17,8 @@
* under the License.
*/
const LOGGING_TAGS = ['stats-collection'];
/*
* @param {Object} server
* @return {Object} helpful logger object
*/
export function getCollectorLogger(server) {
return {
debug: message => server.log(['debug', ...LOGGING_TAGS], message),
info: message => server.log(['info', ...LOGGING_TAGS], message),
warn: message => server.log(['warning', ...LOGGING_TAGS], message)
};
}
import { schema } from '@kbn/config-schema';
export const ConfigSchema = schema.object({
maximumWaitTimeForAllCollectorsInS: schema.number({ defaultValue: 60 }),
});

View file

@ -17,15 +17,11 @@
* under the License.
*/
import { CollectorSet } from './classes';
import { PluginInitializerContext } from '../../../../src/core/server';
import { Plugin } from './plugin';
import { ConfigSchema } from './config';
export function usageMixin(kbnServer, server, config) {
const collectorSet = new CollectorSet(server, undefined, config);
/*
* expose the collector set object on the server
* provides factory methods for feature owners to create their own collector objects
* use collectorSet.register(collector) to register your feature's collector object(s)
*/
server.decorate('server', 'usage', { collectorSet });
}
export { UsageCollectionSetup } from './plugin';
export const config = { schema: ConfigSchema };
export const plugin = (initializerContext: PluginInitializerContext) =>
new Plugin(initializerContext);

View file

@ -0,0 +1,55 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { first } from 'rxjs/operators';
import { TypeOf } from '@kbn/config-schema';
import { ConfigSchema } from './config';
import { PluginInitializerContext, Logger } from '../../../../src/core/server';
import { CollectorSet } from './collector';
export type UsageCollectionSetup = CollectorSet;
export class Plugin {
logger: Logger;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
}
public async setup(): Promise<UsageCollectionSetup> {
const config = await this.initializerContext.config
.create<TypeOf<typeof ConfigSchema>>()
.pipe(first())
.toPromise();
const collectorSet = new CollectorSet({
logger: this.logger,
maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS,
});
return collectorSet;
}
public start() {
this.logger.debug('Starting plugin');
}
public stop() {
this.logger.debug('Stopping plugin');
}
}

View file

@ -108,7 +108,8 @@ export const apm: LegacyPluginInitializer = kibana => {
}
}
});
makeApmUsageCollector(server);
const { usageCollection } = server.newPlatform.setup.plugins;
makeApmUsageCollector(usageCollection, server);
const apmPlugin = server.newPlatform.setup.plugins
.apm as APMPluginContract;

View file

@ -13,6 +13,7 @@ import {
APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID
} from '../../../common/apm_saved_object_constants';
import { APMLegacyServer } from '../../routes/typings';
import { UsageCollectionSetup } from '../../../../../../../src/plugins/usage_collection/server';
export function createApmTelementry(
agentNames: string[] = []
@ -43,8 +44,11 @@ export async function storeApmServicesTelemetry(
}
}
export function makeApmUsageCollector(server: APMLegacyServer) {
const apmUsageCollector = server.usage.collectorSet.makeUsageCollector({
export function makeApmUsageCollector(
usageCollector: UsageCollectionSetup,
server: APMLegacyServer
) {
const apmUsageCollector = usageCollector.makeUsageCollector({
type: 'apm',
fetch: async () => {
const internalSavedObjectsClient = getInternalSavedObjectsClient(server);
@ -60,5 +64,6 @@ export function makeApmUsageCollector(server: APMLegacyServer) {
},
isReady: () => true
});
server.usage.collectorSet.register(apmUsageCollector);
usageCollector.registerCollector(apmUsageCollector);
}

View file

@ -49,13 +49,7 @@ export interface Route<
}) => Promise<TReturn>;
}
export type APMLegacyServer = Pick<Server, 'usage' | 'savedObjects' | 'log'> & {
usage: {
collectorSet: {
makeUsageCollector: (options: unknown) => unknown;
register: (options: unknown) => unknown;
};
};
export type APMLegacyServer = Pick<Server, 'savedObjects' | 'log'> & {
plugins: {
elasticsearch: Server['plugins']['elasticsearch'];
};

View file

@ -29,12 +29,6 @@ export class Plugin {
has: key => has(config, key),
}),
route: def => this.routes.push(def),
usage: {
collectorSet: {
makeUsageCollector: () => {},
register: () => {},
},
},
};
const { init } = this.props;

View file

@ -61,7 +61,7 @@ export class Plugin {
},
});
registerCanvasUsageCollector(core, plugins);
registerCanvasUsageCollector(plugins.usageCollection, core);
loadSampleData(
plugins.sampleData.addSavedObjectsToSampleDataset,
plugins.sampleData.addAppLinksToSampleDataset

View file

@ -8,6 +8,7 @@ import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch';
import { Legacy } from 'kibana';
import { CoreSetup as ExistingCoreSetup } from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { PluginSetupContract } from '../../../../plugins/features/server';
export interface CoreSetup {
@ -32,7 +33,7 @@ export interface PluginsSetup {
addSavedObjectsToSampleDataset: any;
addAppLinksToSampleDataset: any;
};
usage: Legacy.Server['usage'];
usageCollection: UsageCollectionSetup;
}
export async function createSetupShim(
@ -68,7 +69,7 @@ export async function createSetupShim(
// @ts-ignore: Missing from Legacy Server Type
addAppLinksToSampleDataset: server.addAppLinksToSampleDataset,
},
usage: server.usage,
usageCollection: server.newPlatform.setup.plugins.usageCollection,
},
};
}

View file

@ -5,7 +5,8 @@
*/
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { CoreSetup, PluginsSetup } from '../shim';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CoreSetup } from '../shim';
// @ts-ignore missing local declaration
import { CANVAS_USAGE_TYPE } from '../../common/lib/constants';
import { workpadCollector } from './workpad_collector';
@ -22,9 +23,12 @@ const collectors: TelemetryCollector[] = [workpadCollector, customElementCollect
A usage collector function returns an object derived from current data in the ES Cluster.
*/
export function registerCanvasUsageCollector(setup: CoreSetup, plugins: PluginsSetup) {
const kibanaIndex = setup.getServerConfig().get<string>('kibana.index');
const canvasCollector = plugins.usage.collectorSet.makeUsageCollector({
export function registerCanvasUsageCollector(
usageCollection: UsageCollectionSetup,
core: CoreSetup
) {
const kibanaIndex = core.getServerConfig().get<string>('kibana.index');
const canvasCollector = usageCollection.makeUsageCollector({
type: CANVAS_USAGE_TYPE,
isReady: () => true,
fetch: async (callCluster: CallCluster) => {
@ -42,5 +46,5 @@ export function registerCanvasUsageCollector(setup: CoreSetup, plugins: PluginsS
},
});
plugins.usage.collectorSet.register(canvasCollector);
usageCollection.registerCollector(canvasCollector);
}

View file

@ -5,37 +5,39 @@
*/
import sinon from 'sinon';
import {
createCollectorFetch,
getCloudUsageCollector,
KibanaHapiServer,
} from './get_cloud_usage_collector';
import { Server } from 'hapi';
import { createCollectorFetch, createCloudUsageCollector } from './cloud_usage_collector';
const CLOUD_ID_STAGING =
'staging:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==';
const CLOUD_ID =
'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==';
const getMockServer = (cloudId?: string) => ({
usage: { collectorSet: { makeUsageCollector: sinon.stub() } },
config() {
return {
get(path: string) {
switch (path) {
case 'xpack.cloud':
return { id: cloudId };
default:
throw Error(`server.config().get(${path}) should not be called by this collector.`);
}
},
};
},
const mockUsageCollection = () => ({
makeUsageCollector: sinon.stub(),
});
const getMockServer = (cloudId?: string) =>
({
config() {
return {
get(path: string) {
switch (path) {
case 'xpack.cloud':
return { id: cloudId };
default:
throw Error(`server.config().get(${path}) should not be called by this collector.`);
}
},
};
},
} as Server);
describe('Cloud usage collector', () => {
describe('collector', () => {
it('returns `isCloudEnabled: false` if `xpack.cloud.id` is not defined', async () => {
const collector = await createCollectorFetch(getMockServer())();
const mockServer = getMockServer();
const collector = await createCollectorFetch(mockServer)();
expect(collector.isCloudEnabled).toBe(false);
});
@ -48,11 +50,11 @@ describe('Cloud usage collector', () => {
});
});
describe('getCloudUsageCollector', () => {
it('returns calls `collectorSet.makeUsageCollector`', () => {
describe('createCloudUsageCollector', () => {
it('returns calls `makeUsageCollector`', () => {
const mockServer = getMockServer();
getCloudUsageCollector((mockServer as any) as KibanaHapiServer);
const { makeUsageCollector } = mockServer.usage.collectorSet;
expect(makeUsageCollector.calledOnce).toBe(true);
const usageCollection = mockUsageCollection();
createCloudUsageCollector(usageCollection as any, mockServer);
expect(usageCollection.makeUsageCollector.calledOnce).toBe(true);
});
});

View file

@ -5,21 +5,14 @@
*/
import { Server } from 'hapi';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { KIBANA_CLOUD_STATS_TYPE } from './constants';
export interface UsageStats {
isCloudEnabled: boolean;
}
export interface KibanaHapiServer extends Server {
usage: {
collectorSet: {
makeUsageCollector: any;
};
};
}
export function createCollectorFetch(server: any) {
export function createCollectorFetch(server: Server) {
return async function fetchUsageStats(): Promise<UsageStats> {
const { id } = server.config().get(`xpack.cloud`);
@ -29,15 +22,15 @@ export function createCollectorFetch(server: any) {
};
}
/*
* @param {Object} server
* @return {Object} kibana usage stats type collection object
*/
export function getCloudUsageCollector(server: KibanaHapiServer) {
const { collectorSet } = server.usage;
return collectorSet.makeUsageCollector({
export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) {
return usageCollection.makeUsageCollector({
type: KIBANA_CLOUD_STATS_TYPE,
isReady: () => true,
fetch: createCollectorFetch(server),
});
}
export function registerCloudUsageCollector(usageCollection: UsageCollectionSetup, server: Server) {
const collector = createCloudUsageCollector(usageCollection, server);
usageCollection.registerCollector(collector);
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getCloudUsageCollector } from './get_cloud_usage_collector';
import { registerCloudUsageCollector } from './cloud_usage_collector';
export const cloud = kibana => {
return new kibana.Plugin({
@ -40,7 +40,8 @@ export const cloud = kibana => {
server.expose('config', {
isCloudEnabled: !!config.id
});
server.usage.collectorSet.register(getCloudUsageCollector(server));
const { usageCollection } = server.newPlatform.setup.plugins;
registerCloudUsageCollector(usageCollection, server);
}
});
};

View file

@ -22,7 +22,10 @@ export const fileUpload = kibana => {
init(server) {
const coreSetup = server.newPlatform.setup.core;
const pluginsSetup = {};
const { usageCollection } = server.newPlatform.setup.plugins;
const pluginsSetup = {
usageCollection,
};
// legacy dependencies
const __LEGACY = {
@ -33,11 +36,6 @@ export const fileUpload = kibana => {
savedObjects: {
getSavedObjectsRepository: server.savedObjects.getSavedObjectsRepository
},
usage: {
collectorSet: {
makeUsageCollector: server.usage.collectorSet.makeUsageCollector
}
}
};
new FileUploadPlugin().setup(coreSetup, pluginsSetup, __LEGACY);

View file

@ -5,16 +5,13 @@
*/
import { getImportRouteHandler } from './routes/file_upload';
import { getTelemetry, initTelemetry } from './telemetry/telemetry';
import { MAX_BYTES } from '../common/constants/file_import';
const TELEMETRY_TYPE = 'fileUploadTelemetry';
import { registerFileUploadUsageCollector } from './telemetry';
export class FileUploadPlugin {
setup(core, plugins, __LEGACY) {
const elasticsearchPlugin = __LEGACY.plugins.elasticsearch;
const getSavedObjectsRepository = __LEGACY.savedObjects.getSavedObjectsRepository;
const makeUsageCollector = __LEGACY.usage.collectorSet.makeUsageCollector;
// Set up route
__LEGACY.route({
@ -26,11 +23,9 @@ export class FileUploadPlugin {
}
});
// Make usage collector
makeUsageCollector({
type: TELEMETRY_TYPE,
isReady: () => true,
fetch: async () => (await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry()
registerFileUploadUsageCollector(plugins.usageCollection, {
elasticsearchPlugin,
getSavedObjectsRepository,
});
}
}

View file

@ -0,0 +1,28 @@
/*
* 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getTelemetry, initTelemetry } from './telemetry';
const TELEMETRY_TYPE = 'fileUploadTelemetry';
export function registerFileUploadUsageCollector(
usageCollection: UsageCollectionSetup,
deps: {
elasticsearchPlugin: any;
getSavedObjectsRepository: any;
}
): void {
const { elasticsearchPlugin, getSavedObjectsRepository } = deps;
const fileUploadUsageCollector = usageCollection.makeUsageCollector({
type: TELEMETRY_TYPE,
isReady: () => true,
fetch: async () =>
(await getTelemetry(elasticsearchPlugin, getSavedObjectsRepository)) || initTelemetry(),
});
usageCollection.registerCollector(fileUploadUsageCollector);
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './telemetry';
export { registerFileUploadUsageCollector } from './file_upload_usage_collector';

View file

@ -13,11 +13,8 @@ import { UsageCollector } from './usage/usage_collector';
import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view';
import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view';
export interface KbnServer extends Server {
usage: any;
}
export const initServerWithKibana = (kbnServer: KbnServer) => {
export const initServerWithKibana = (kbnServer: Server) => {
const { usageCollection } = kbnServer.newPlatform.setup.plugins;
const libs = compose(kbnServer);
initInfraServer(libs);
@ -27,7 +24,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
);
// Register a function with server to manage the collection of usage stats
kbnServer.usage.collectorSet.register(UsageCollector.getUsageCollector(kbnServer));
UsageCollector.registerUsageCollector(usageCollection);
const xpackMainPlugin = kbnServer.plugins.xpack_main;
xpackMainPlugin.registerFeature({

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { InfraNodeType } from '../graphql/types';
import { KbnServer } from '../kibana.index';
const KIBANA_REPORTING_TYPE = 'infraops';
interface InfraopsSum {
@ -17,10 +16,13 @@ interface InfraopsSum {
}
export class UsageCollector {
public static getUsageCollector(server: KbnServer) {
const { collectorSet } = server.usage;
public static registerUsageCollector(usageCollection: UsageCollectionSetup): void {
const collector = UsageCollector.getUsageCollector(usageCollection);
usageCollection.registerCollector(collector);
}
return collectorSet.makeUsageCollector({
public static getUsageCollector(usageCollection: UsageCollectionSetup) {
return usageCollection.makeUsageCollector({
type: KIBANA_REPORTING_TYPE,
isReady: () => true,
fetch: async () => {

View file

@ -58,10 +58,12 @@ export const lens: LegacyPluginInitializer = kibana => {
// Set up with the new platform plugin lifecycle API.
const plugin = lensServerPlugin();
const { usageCollection } = server.newPlatform.setup.plugins;
plugin.setup(kbnServer.newPlatform.setup.core, {
usageCollection,
// Legacy APIs
savedObjects: server.savedObjects,
usage: server.usage,
config: server.config(),
server,
});

View file

@ -6,27 +6,22 @@
import { Server, KibanaConfig } from 'src/legacy/server/kbn_server';
import { Plugin, CoreSetup, SavedObjectsLegacyService } from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { setupRoutes } from './routes';
import { registerLensUsageCollector, initializeLensTelemetry } from './usage';
export interface PluginSetupContract {
savedObjects: SavedObjectsLegacyService;
usageCollection: UsageCollectionSetup;
config: KibanaConfig;
server: Server;
}
export class LensServer implements Plugin<{}, {}, {}, {}> {
setup(
core: CoreSetup,
plugins: {
savedObjects: SavedObjectsLegacyService;
usage: {
collectorSet: {
makeUsageCollector: (options: unknown) => unknown;
register: (options: unknown) => unknown;
};
};
config: KibanaConfig;
server: Server;
}
) {
setup(core: CoreSetup, plugins: PluginSetupContract) {
setupRoutes(core, plugins);
registerLensUsageCollector(core, plugins);
initializeLensTelemetry(core, plugins);
registerLensUsageCollector(plugins.usageCollection, plugins.server);
initializeLensTelemetry(core, plugins.server);
return {};
}

View file

@ -6,29 +6,17 @@
import moment from 'moment';
import { get } from 'lodash';
import { Server, KibanaConfig } from 'src/legacy/server/kbn_server';
import { CoreSetup, SavedObjectsLegacyService } from 'src/core/server';
import { Server } from 'src/legacy/server/kbn_server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { LensUsage, LensTelemetryState } from './types';
export function registerLensUsageCollector(
core: CoreSetup,
plugins: {
savedObjects: SavedObjectsLegacyService;
usage: {
collectorSet: {
makeUsageCollector: (options: unknown) => unknown;
register: (options: unknown) => unknown;
};
};
config: KibanaConfig;
server: Server;
}
) {
export function registerLensUsageCollector(usageCollection: UsageCollectionSetup, server: Server) {
let isCollectorReady = false;
async function determineIfTaskManagerIsReady() {
let isReady = false;
try {
isReady = await isTaskManagerReady(plugins.server);
isReady = await isTaskManagerReady(server);
} catch (err) {} // eslint-disable-line
if (isReady) {
@ -39,11 +27,11 @@ export function registerLensUsageCollector(
}
determineIfTaskManagerIsReady();
const lensUsageCollector = plugins.usage.collectorSet.makeUsageCollector({
const lensUsageCollector = usageCollection.makeUsageCollector({
type: 'lens',
fetch: async (): Promise<LensUsage> => {
try {
const docs = await getLatestTaskState(plugins.server);
const docs = await getLatestTaskState(server);
// get the accumulated state from the recurring task
const state: LensTelemetryState = get(docs, '[0].state');
@ -75,7 +63,8 @@ export function registerLensUsageCollector(
},
isReady: () => isCollectorReady,
});
plugins.usage.collectorSet.register(lensUsageCollector);
usageCollection.registerCollector(lensUsageCollector);
}
function addEvents(prevEvents: Record<string, number>, newEvents: Record<string, number>) {

View file

@ -39,12 +39,12 @@ type ClusterDeleteType = (
options?: CallClusterOptions
) => Promise<DeleteDocumentByQueryResponse>;
export function initializeLensTelemetry(core: CoreSetup, { server }: { server: Server }) {
registerLensTelemetryTask(core, { server });
export function initializeLensTelemetry(core: CoreSetup, server: Server) {
registerLensTelemetryTask(core, server);
scheduleTasks(server);
}
function registerLensTelemetryTask(core: CoreSetup, { server }: { server: Server }) {
function registerLensTelemetryTask(core: CoreSetup, server: Server) {
const taskManager = server.plugins.task_manager;
if (!taskManager) {

View file

@ -101,12 +101,12 @@ export function maps(kibana) {
init(server) {
const mapsEnabled = server.config().get('xpack.maps.enabled');
const { usageCollection } = server.newPlatform.setup.plugins;
if (!mapsEnabled) {
server.log(['info', 'maps'], 'Maps app disabled by configuration');
return;
}
initTelemetryCollection(server);
initTelemetryCollection(usageCollection, server);
const xpackMainPlugin = server.plugins.xpack_main;
let routesInitialized = false;

View file

@ -7,10 +7,10 @@
import _ from 'lodash';
import { TASK_ID, scheduleTask, registerMapsTelemetryTask } from './telemetry_task';
export function initTelemetryCollection(server) {
export function initTelemetryCollection(usageCollection, server) {
registerMapsTelemetryTask(server);
scheduleTask(server);
registerMapsUsageCollector(server);
registerMapsUsageCollector(usageCollection, server);
}
async function isTaskManagerReady(server) {
@ -81,9 +81,8 @@ export function buildCollectorObj(server) {
};
}
export function registerMapsUsageCollector(server) {
export function registerMapsUsageCollector(usageCollection, server) {
const collectorObj = buildCollectorObj(server);
const mapsUsageCollector = server.usage.collectorSet
.makeUsageCollector(collectorObj);
server.usage.collectorSet.register(mapsUsageCollector);
const mapsUsageCollector = usageCollection.makeUsageCollector(collectorObj);
usageCollection.registerCollector(mapsUsageCollector);
}

View file

@ -40,12 +40,6 @@ export const getMockKbnServer = (
fetch: mockTaskFetch,
},
},
usage: {
collectorSet: {
makeUsageCollector: () => '',
register: () => undefined,
},
},
config: () => ({ get: () => '' }),
log: () => undefined
});

View file

@ -79,7 +79,6 @@ export const ml = (kibana: any) => {
injectUiAppVars: server.injectUiAppVars,
http: mlHttpService,
savedObjects: server.savedObjects,
usage: server.usage,
};
const plugins = {
@ -87,6 +86,7 @@ export const ml = (kibana: any) => {
security: server.plugins.security,
xpackMain: server.plugins.xpack_main,
spaces: server.plugins.spaces,
usageCollection: kbnServer.newPlatform.setup.plugins.usageCollection,
ml: this,
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import {
createMlTelemetry,
getSavedObjectsClient,
@ -14,12 +15,11 @@ import {
import { UsageInitialization } from '../../new_platform/plugin';
export function makeMlUsageCollector({
elasticsearchPlugin,
usage,
savedObjects,
}: UsageInitialization): void {
const mlUsageCollector = usage.collectorSet.makeUsageCollector({
export function makeMlUsageCollector(
usageCollection: UsageCollectionSetup,
{ elasticsearchPlugin, savedObjects }: UsageInitialization
): void {
const mlUsageCollector = usageCollection.makeUsageCollector({
type: 'ml',
isReady: () => true,
fetch: async (): Promise<MlTelemetry> => {
@ -35,5 +35,6 @@ export function makeMlUsageCollector({
}
},
});
usage.collectorSet.register(mlUsageCollector);
usageCollection.registerCollector(mlUsageCollector);
}

View file

@ -10,6 +10,7 @@ import { ServerRoute } from 'hapi';
import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_server';
import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server';
import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { XPackMainPlugin } from '../../../xpack_main/xpack_main';
import { addLinksToSampleDatasets } from '../lib/sample_data_sets';
import { checkLicense } from '../lib/check_license';
@ -68,12 +69,6 @@ export interface MlCoreSetup {
injectUiAppVars: (id: string, callback: () => {}) => any;
http: MlHttpServiceSetup;
savedObjects: SavedObjectsLegacyService;
usage: {
collectorSet: {
makeUsageCollector: any;
register: (collector: any) => void;
};
};
}
export interface MlInitializerContext extends PluginInitializerContext {
legacyConfig: KibanaConfig;
@ -84,6 +79,7 @@ export interface PluginsSetup {
xpackMain: MlXpackMainPlugin;
security: any;
spaces: any;
usageCollection: UsageCollectionSetup;
// TODO: this is temporary for `mirrorPluginStatus`
ml: any;
}
@ -98,12 +94,6 @@ export interface RouteInitialization {
}
export interface UsageInitialization {
elasticsearchPlugin: ElasticsearchPlugin;
usage: {
collectorSet: {
makeUsageCollector: any;
register: (collector: any) => void;
};
};
savedObjects: SavedObjectsLegacyService;
}
@ -201,10 +191,8 @@ export class Plugin {
savedObjects: core.savedObjects,
spacesPlugin: plugins.spaces,
};
const usageInitializationDeps: UsageInitialization = {
elasticsearchPlugin: plugins.elasticsearch,
usage: core.usage,
savedObjects: core.savedObjects,
};
@ -231,7 +219,7 @@ export class Plugin {
fileDataVisualizerRoutes(extendedRouteInitializationDeps);
initMlServerLog(logInitializationDeps);
makeMlUsageCollector(usageInitializationDeps);
makeMlUsageCollector(plugins.usageCollection, usageInitializationDeps);
}
public stop() {}

View file

@ -56,9 +56,6 @@ export const monitoring = (kibana) => new kibana.Plugin({
throw `Unknown key '${key}'`;
}
}),
usage: {
collectorSet: server.usage.collectorSet
},
injectUiAppVars: server.injectUiAppVars,
log: (...args) => server.log(...args),
getOSInfo: server.getOSInfo,
@ -70,11 +67,12 @@ export const monitoring = (kibana) => new kibana.Plugin({
_hapi: server,
_kbnServer: this.kbnServer
};
const { usageCollection } = server.newPlatform.setup.plugins;
const plugins = {
xpack_main: server.plugins.xpack_main,
elasticsearch: server.plugins.elasticsearch,
infra: server.plugins.infra,
usageCollection,
};
new Plugin().setup(serverFacade, plugins);

View file

@ -68,21 +68,21 @@ export class BulkUploader {
/*
* Start the interval timer
* @param {CollectorSet} collectorSet object to use for initial the fetch/upload and fetch/uploading on interval
* @param {usageCollection} usageCollection object to use for initial the fetch/upload and fetch/uploading on interval
* @return undefined
*/
start(collectorSet) {
start(usageCollection) {
this._log.info('Starting monitoring stats collection');
const filterCollectorSet = _collectorSet => {
const filterCollectorSet = _usageCollection => {
const successfulUploadInLastDay = this._lastFetchUsageTime && this._lastFetchUsageTime + this._usageInterval > Date.now();
return _collectorSet.getFilteredCollectorSet(c => {
return _usageCollection.getFilteredCollectorSet(c => {
// this is internal bulk upload, so filter out API-only collectors
if (c.ignoreForInternalUploader) {
return false;
}
// Only collect usage data at the same interval as telemetry would (default to once a day)
if (successfulUploadInLastDay && _collectorSet.isUsageCollector(c)) {
if (successfulUploadInLastDay && _usageCollection.isUsageCollector(c)) {
return false;
}
return true;
@ -92,11 +92,11 @@ export class BulkUploader {
if (this._timer) {
clearInterval(this._timer);
} else {
this._fetchAndUpload(filterCollectorSet(collectorSet)); // initial fetch
this._fetchAndUpload(filterCollectorSet(usageCollection)); // initial fetch
}
this._timer = setInterval(() => {
this._fetchAndUpload(filterCollectorSet(collectorSet));
this._fetchAndUpload(filterCollectorSet(usageCollection));
}, this._interval);
}
@ -121,12 +121,12 @@ export class BulkUploader {
}
/*
* @param {CollectorSet} collectorSet
* @param {usageCollection} usageCollection
* @return {Promise} - resolves to undefined
*/
async _fetchAndUpload(collectorSet) {
const collectorsReady = await collectorSet.areAllCollectorsReady();
const hasUsageCollectors = collectorSet.some(collectorSet.isUsageCollector);
async _fetchAndUpload(usageCollection) {
const collectorsReady = await usageCollection.areAllCollectorsReady();
const hasUsageCollectors = usageCollection.some(usageCollection.isUsageCollector);
if (!collectorsReady) {
this._log.debug('Skipping bulk uploading because not all collectors are ready');
if (hasUsageCollectors) {
@ -136,8 +136,8 @@ export class BulkUploader {
return;
}
const data = await collectorSet.bulkFetch(this._callClusterWithInternalUser);
const payload = this.toBulkUploadFormat(compact(data), collectorSet);
const data = await usageCollection.bulkFetch(this._callClusterWithInternalUser);
const payload = this.toBulkUploadFormat(compact(data), usageCollection);
if (payload) {
try {
@ -202,7 +202,7 @@ export class BulkUploader {
* }
* ]
*/
toBulkUploadFormat(rawData, collectorSet) {
toBulkUploadFormat(rawData, usageCollection) {
if (rawData.length === 0) {
return;
}
@ -210,7 +210,7 @@ export class BulkUploader {
// convert the raw data to a nested object by taking each payload through
// its formatter, organizing it per-type
const typesNested = rawData.reduce((accum, { type, result }) => {
const { type: uploadType, payload: uploadData } = collectorSet.getCollectorByType(type).formatForBulkUpload(result);
const { type: uploadType, payload: uploadData } = usageCollection.getCollectorByType(type).formatForBulkUpload(result);
return defaultsDeep(accum, { [uploadType]: uploadData });
}, {});
// convert the nested object into a flat array, with each payload prefixed

View file

@ -19,8 +19,8 @@ const TYPES = [
/**
* Fetches saved object counts by querying the .kibana index
*/
export function getKibanaUsageCollector({ collectorSet, config }) {
return collectorSet.makeUsageCollector({
export function getKibanaUsageCollector(usageCollection, config) {
return usageCollection.makeUsageCollector({
type: KIBANA_USAGE_TYPE,
isReady: () => true,
async fetch(callCluster) {

View file

@ -49,14 +49,13 @@ class OpsMonitor {
/*
* Initialize a collector for Kibana Ops Stats
*/
export function getOpsStatsCollector({
export function getOpsStatsCollector(usageCollection, {
elasticsearchPlugin,
kbnServerConfig,
log,
config,
getOSInfo,
hapiServer,
collectorSet
}) {
const buffer = opsBuffer({ log, config, getOSInfo });
const interval = kbnServerConfig.get('ops.interval');
@ -85,7 +84,7 @@ export function getOpsStatsCollector({
}, 5 * 1000); // wait 5 seconds to avoid race condition with reloading logging configuration
});
return collectorSet.makeStatsCollector({
return usageCollection.makeStatsCollector({
type: KIBANA_STATS_TYPE_MONITORING,
init: opsMonitor.start,
isReady: () => {

View file

@ -46,8 +46,8 @@ export async function checkForEmailValue(
}
}
export function getSettingsCollector({ config, collectorSet }) {
return collectorSet.makeStatsCollector({
export function getSettingsCollector(usageCollection, config) {
return usageCollection.makeStatsCollector({
type: KIBANA_SETTINGS_TYPE,
isReady: () => true,
async fetch(callCluster) {

View file

@ -4,6 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { getKibanaUsageCollector } from './get_kibana_usage_collector';
export { getOpsStatsCollector } from './get_ops_stats_collector';
export { getSettingsCollector } from './get_settings_collector';
import { getKibanaUsageCollector } from './get_kibana_usage_collector';
import { getOpsStatsCollector } from './get_ops_stats_collector';
import { getSettingsCollector } from './get_settings_collector';
export function registerCollectors(usageCollection, collectorsConfigs) {
const { config } = collectorsConfigs;
usageCollection.registerCollector(getOpsStatsCollector(usageCollection, collectorsConfigs));
usageCollection.registerCollector(getKibanaUsageCollector(usageCollection, config));
usageCollection.registerCollector(getSettingsCollector(usageCollection, config));
}

View file

@ -5,3 +5,4 @@
*/
export { initBulkUploader } from './init';
export { registerCollectors } from './collectors';

View file

@ -13,6 +13,17 @@ const liveClusterUuid = 'a12';
const mockReq = (searchResult = {}) => {
return {
server: {
newPlatform: {
setup: {
plugins: {
usageCollection: {
getCollectorByType: () => ({
isReady: () => false
}),
},
},
},
},
config() {
return {
get: sinon.stub()

View file

@ -273,13 +273,15 @@ function shouldSkipBucket(product, bucket) {
return false;
}
async function getLiveKibanaInstance(req) {
const { collectorSet } = req.server.usage;
const kibanaStatsCollector = collectorSet.getCollectorByType(KIBANA_STATS_TYPE);
async function getLiveKibanaInstance(usageCollection) {
if (!usageCollection) {
return null;
}
const kibanaStatsCollector = usageCollection.getCollectorByType(KIBANA_STATS_TYPE);
if (!await kibanaStatsCollector.isReady()) {
return null;
}
return collectorSet.toApiFieldNames(await kibanaStatsCollector.fetch());
return usageCollection.toApiFieldNames(await kibanaStatsCollector.fetch());
}
async function getLiveElasticsearchClusterUuid(req) {
@ -341,9 +343,11 @@ async function getLiveElasticsearchCollectionEnabled(req) {
* @param {*} skipLiveData Optional and will not make any live api calls if set to true
*/
export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeUuid, skipLiveData) => {
const config = req.server.config();
const kibanaUuid = config.get('server.uuid');
const hasPermissions = await hasNecessaryPermissions(req);
if (!hasPermissions) {
return {
_meta: {
@ -351,6 +355,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU
}
};
}
console.log('OKOKOKOK');
const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req);
const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid;
@ -372,7 +377,8 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU
const liveEsNodes = skipLiveData || !isLiveCluster ? [] : await getLivesNodes(req);
const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(req);
const { usageCollection } = req.server.newPlatform.setup.plugins;
const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(usageCollection);
const indicesBuckets = get(recentDocuments, 'aggregations.indices.buckets', []);
const liveClusterInternalCollectionEnabled = await getLiveElasticsearchCollectionEnabled(req);

View file

@ -9,35 +9,27 @@ import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../common/constants'
import { requireUIRoutes } from './routes';
import { instantiateClient } from './es_client/instantiate_client';
import { initMonitoringXpackInfo } from './init_monitoring_xpack_info';
import { initBulkUploader } from './kibana_monitoring';
import { initBulkUploader, registerCollectors } from './kibana_monitoring';
import { registerMonitoringCollection } from './telemetry_collection';
import {
getKibanaUsageCollector,
getOpsStatsCollector,
getSettingsCollector,
} from './kibana_monitoring/collectors';
export class Plugin {
setup(core, plugins) {
const kbnServer = core._kbnServer;
const config = core.config();
const { collectorSet } = core.usage;
const usageCollection = plugins.usageCollection;
registerMonitoringCollection();
/*
* Register collector objects for stats to show up in the APIs
*/
collectorSet.register(getOpsStatsCollector({
registerCollectors(usageCollection, {
elasticsearchPlugin: plugins.elasticsearch,
kbnServerConfig: kbnServer.config,
log: core.log,
config,
getOSInfo: core.getOSInfo,
hapiServer: core._hapi,
collectorSet: core.usage.collectorSet,
}));
collectorSet.register(getKibanaUsageCollector({ collectorSet, config }));
collectorSet.register(getSettingsCollector({ collectorSet, config }));
registerMonitoringCollection();
});
/*
* Instantiate and start the internal background task that calls collector
@ -110,7 +102,7 @@ export class Plugin {
const mainMonitoring = xpackMainInfo.feature('monitoring');
const monitoringBulkEnabled = mainMonitoring && mainMonitoring.isAvailable() && mainMonitoring.isEnabled();
if (monitoringBulkEnabled) {
bulkUploader.start(collectorSet);
bulkUploader.start(usageCollection);
} else {
bulkUploader.handleNotEnabled();
}

View file

@ -8,9 +8,8 @@ import expect from '@kbn/expect';
import sinon from 'sinon';
import { getClusterUuids, fetchClusterUuids, handleClusterUuidsResponse } from '../get_cluster_uuids';
// FAILING: https://github.com/elastic/kibana/issues/51371
describe.skip('get_cluster_uuids', () => {
const callWith = sinon.stub();
describe('get_cluster_uuids', () => {
const callCluster = sinon.stub();
const size = 123;
const server = {
config: sinon.stub().returns({
@ -29,23 +28,23 @@ describe.skip('get_cluster_uuids', () => {
}
}
};
const expectedUuids = response.aggregations.cluster_uuids.buckets.map(bucket => bucket.key);
const expectedUuids = response.aggregations.cluster_uuids.buckets
.map(bucket => bucket.key)
.map(expectedUuid => ({ clusterUuid: expectedUuid }));
const start = new Date();
const end = new Date();
describe('getClusterUuids', () => {
it('returns cluster UUIDs', async () => {
callWith.withArgs('search').returns(Promise.resolve(response));
expect(await getClusterUuids(server, callWith, start, end)).to.eql(expectedUuids);
callCluster.withArgs('search').returns(Promise.resolve(response));
expect(await getClusterUuids({ server, callCluster, start, end })).to.eql(expectedUuids);
});
});
describe('fetchClusterUuids', () => {
it('searches for clusters', async () => {
callWith.returns(Promise.resolve(response));
expect(await fetchClusterUuids(server, callWith, start, end)).to.be(response);
callCluster.returns(Promise.resolve(response));
expect(await fetchClusterUuids({ server, callCluster, start, end })).to.be(response);
});
});
@ -53,13 +52,11 @@ describe.skip('get_cluster_uuids', () => {
// filterPath makes it easy to ignore anything unexpected because it will come back empty
it('handles unexpected response', () => {
const clusterUuids = handleClusterUuidsResponse({});
expect(clusterUuids.length).to.be(0);
});
it('handles valid response', () => {
const clusterUuids = handleClusterUuidsResponse(response);
expect(clusterUuids).to.eql(expectedUuids);
});

View file

@ -54,12 +54,6 @@ export interface HapiServer {
}>;
};
};
usage: {
collectorSet: {
register: (collector: any) => void;
makeUsageCollector: (collectorOpts: any) => void;
};
};
config: () => {
get: (prop: string) => any;
};

View file

@ -15,7 +15,8 @@ export const ossTelemetry = (kibana) => {
configPrefix: 'xpack.oss_telemetry',
init(server) {
registerCollectors(server);
const { usageCollection } = server.newPlatform.setup.plugins;
registerCollectors(usageCollection, server);
registerTasks(server);
scheduleTasks(server);
}

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { HapiServer } from '../../../';
import { registerVisualizationsCollector } from './visualizations/register_usage_collector';
export function registerCollectors(server: HapiServer) {
registerVisualizationsCollector(server);
export function registerCollectors(usageCollection: UsageCollectionSetup, server: HapiServer) {
registerVisualizationsCollector(usageCollection, server);
}

View file

@ -4,11 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
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);
export function registerVisualizationsCollector(
usageCollection: UsageCollectionSetup,
server: HapiServer
): void {
const collector = usageCollection.makeUsageCollector(getUsageCollector(server));
usageCollection.registerCollector(collector);
}

View file

@ -54,12 +54,6 @@ export const getMockKbnServer = (
fetch: mockTaskFetch,
},
},
usage: {
collectorSet: {
makeUsageCollector: () => '',
register: () => undefined,
},
},
config: () => mockConfig,
log: () => undefined,
});

View file

@ -20,7 +20,7 @@ import {
import { config as reportingConfig } from './config';
import { logConfiguration } from './log_configuration';
import { createBrowserDriverFactory } from './server/browsers';
import { getReportingUsageCollector } from './server/usage';
import { registerReportingUsageCollector } from './server/usage';
import { ReportingConfigOptions, ReportingPluginSpecOptions, ServerFacade } from './types.d';
const kbToBase64Length = (kb: number) => {
@ -76,9 +76,8 @@ export const reporting = (kibana: any) => {
async init(server: ServerFacade) {
let isCollectorReady = false;
// Register a function with server to manage the collection of usage stats
server.usage.collectorSet.register(
getReportingUsageCollector(server, () => isCollectorReady)
);
const { usageCollection } = server.newPlatform.setup.plugins;
registerReportingUsageCollector(usageCollection, server, () => isCollectorReady);
const logger = LevelLogger.createForServer(server, [PLUGIN_ID]);
const [exportTypesRegistry, browserFactory] = await Promise.all([

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { getReportingUsageCollector } from './get_reporting_usage_collector';
export { registerReportingUsageCollector } from './reporting_usage_collector';

View file

@ -4,15 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import sinon from 'sinon';
import { getReportingUsageCollector } from './get_reporting_usage_collector';
import { getReportingUsageCollector } from './reporting_usage_collector';
function getServerMock(customization) {
function getMockUsageCollection() {
class MockUsageCollector {
constructor(_server, { fetch }) {
this.fetch = fetch;
}
}
return {
makeUsageCollector: options => {
return new MockUsageCollector(this, options);
},
};
}
function getServerMock(customization) {
const getLicenseCheckResults = sinon.stub().returns({});
const defaultServerMock = {
plugins: {
@ -44,13 +51,6 @@ function getServerMock(customization) {
}
},
}),
usage: {
collectorSet: {
makeUsageCollector: options => {
return new MockUsageCollector(this, options);
},
},
},
};
return Object.assign(defaultServerMock, customization);
}
@ -66,7 +66,8 @@ describe('license checks', () => {
.stub()
.returns('basic');
const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock()));
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock);
const usageCollection = getMockUsageCollection();
const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
});
@ -91,7 +92,8 @@ describe('license checks', () => {
.stub()
.returns('none');
const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock()));
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithNoLicenseMock);
const usageCollection = getMockUsageCollection();
const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithNoLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
});
@ -116,7 +118,9 @@ describe('license checks', () => {
.stub()
.returns('platinum');
const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock()));
const usageCollection = getMockUsageCollection();
const { fetch: getReportingUsage } = getReportingUsageCollector(
usageCollection,
serverWithPlatinumLicenseMock
);
usageStats = await getReportingUsage(callClusterMock);
@ -143,7 +147,8 @@ describe('license checks', () => {
.stub()
.returns('basic');
const callClusterMock = jest.fn(() => Promise.resolve({}));
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock);
const usageCollection = getMockUsageCollection();
const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
});
@ -160,11 +165,12 @@ describe('license checks', () => {
describe('data modeling', () => {
let getReportingUsage;
beforeAll(async () => {
const usageCollection = getMockUsageCollection();
const serverWithPlatinumLicenseMock = getServerMock();
serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon
.stub()
.returns('platinum');
({ fetch: getReportingUsage } = getReportingUsageCollector(serverWithPlatinumLicenseMock));
({ fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithPlatinumLicenseMock));
});
test('with normal looking usage data', async () => {

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
// @ts-ignore untyped module
import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants';
import { ServerFacade, ESCallCluster } from '../../types';
@ -15,9 +16,12 @@ import { RangeStats } from './types';
* @param {Object} server
* @return {Object} kibana usage stats type collection object
*/
export function getReportingUsageCollector(server: ServerFacade, isReady: () => boolean) {
const { collectorSet } = server.usage;
return collectorSet.makeUsageCollector({
export function getReportingUsageCollector(
usageCollection: UsageCollectionSetup,
server: ServerFacade,
isReady: () => boolean
) {
return usageCollection.makeUsageCollector({
type: KIBANA_REPORTING_TYPE,
isReady,
fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster),
@ -41,3 +45,12 @@ export function getReportingUsageCollector(server: ServerFacade, isReady: () =>
},
});
}
export function registerReportingUsageCollector(
usageCollection: UsageCollectionSetup,
server: ServerFacade,
isReady: () => boolean
) {
const collector = getReportingUsageCollector(usageCollection, server, isReady);
usageCollection.registerCollector(collector);
}

View file

@ -57,12 +57,13 @@ export function rollup(kibana) {
],
},
init: function (server) {
const { usageCollection } = server.newPlatform.setup.plugins;
registerLicenseChecker(server);
registerIndicesRoute(server);
registerFieldsForWildcardRoute(server);
registerSearchRoute(server);
registerJobsRoute(server);
registerRollupUsageCollector(server);
registerRollupUsageCollector(usageCollection, server);
if (
server.plugins.index_management &&
server.plugins.index_management.addIndexManagementDataEnricher

View file

@ -163,10 +163,10 @@ async function fetchRollupVisualizations(kibanaIndex, callCluster, rollupIndexPa
};
}
export function registerRollupUsageCollector(server) {
export function registerRollupUsageCollector(usageCollection, server) {
const kibanaIndex = server.config().get('kibana.index');
const collector = server.usage.collectorSet.makeUsageCollector({
const collector = usageCollection.makeUsageCollector({
type: ROLLUP_USAGE_TYPE,
isReady: () => true,
fetch: async callCluster => {
@ -198,5 +198,5 @@ export function registerRollupUsageCollector(server) {
},
});
server.usage.collectorSet.register(collector);
usageCollection.registerCollector(collector);
}

View file

@ -126,7 +126,6 @@ export const spaces = (kibana: Record<string, any>) =>
kibanaIndex: config.get('kibana.index'),
},
savedObjects: server.savedObjects,
usage: server.usage,
tutorial: {
addScopedTutorialContextFactory: server.addScopedTutorialContextFactory,
},

View file

@ -43,11 +43,12 @@ export function upgradeAssistant(kibana: any) {
init(server: Legacy.Server) {
// Add server routes and initialize the plugin here
const instance = plugin({} as any);
const { usageCollection } = server.newPlatform.setup.plugins;
instance.setup(server.newPlatform.setup.core, {
usageCollection,
__LEGACY: {
// Legacy objects
events: server.events,
usage: server.usage,
savedObjects: server.savedObjects,
// Legacy functions

View file

@ -15,12 +15,6 @@ import { upsertUIOpenOption } from './es_ui_open_apis';
describe('Upgrade Assistant Telemetry SavedObject UIOpen', () => {
const mockIncrementCounter = jest.fn();
const server = jest.fn().mockReturnValue({
usage: {
collectorSet: {
makeUsageCollector: {},
register: {},
},
},
savedObjects: {
getSavedObjectsRepository: jest.fn().mockImplementation(() => {
return {

View file

@ -15,12 +15,6 @@ import { upsertUIReindexOption } from './es_ui_reindex_apis';
describe('Upgrade Assistant Telemetry SavedObject UIReindex', () => {
const mockIncrementCounter = jest.fn();
const server = jest.fn().mockReturnValue({
usage: {
collectorSet: {
makeUsageCollector: {},
register: {},
},
},
savedObjects: {
getSavedObjectsRepository: jest.fn().mockImplementation(() => {
return {

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { makeUpgradeAssistantUsageCollector } from './usage_collector';
export { registerUpgradeAssistantUsageCollector } from './usage_collector';

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import * as usageCollector from './usage_collector';
import { registerUpgradeAssistantUsageCollector } from './usage_collector';
/**
* Since these route callbacks are so thin, these serve simply as integration tests
@ -16,15 +16,16 @@ describe('Upgrade Assistant Usage Collector', () => {
let registerStub: any;
let server: any;
let callClusterStub: any;
let usageCollection: any;
beforeEach(() => {
makeUsageCollectorStub = jest.fn();
registerStub = jest.fn();
usageCollection = {
makeUsageCollector: makeUsageCollectorStub,
registerCollector: registerStub,
};
server = jest.fn().mockReturnValue({
usage: {
collectorSet: { makeUsageCollector: makeUsageCollectorStub, register: registerStub },
register: {},
},
savedObjects: {
getSavedObjectsRepository: jest.fn().mockImplementation(() => {
return {
@ -55,20 +56,20 @@ describe('Upgrade Assistant Usage Collector', () => {
});
});
describe('makeUpgradeAssistantUsageCollector', () => {
it('should call collectorSet.register', () => {
usageCollector.makeUpgradeAssistantUsageCollector(server());
describe('registerUpgradeAssistantUsageCollector', () => {
it('should registerCollector', () => {
registerUpgradeAssistantUsageCollector(usageCollection, server());
expect(registerStub).toHaveBeenCalledTimes(1);
});
it('should call makeUsageCollector with type = upgrade-assistant', () => {
usageCollector.makeUpgradeAssistantUsageCollector(server());
registerUpgradeAssistantUsageCollector(usageCollection, server());
expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1);
expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('upgrade-assistant-telemetry');
});
it('fetchUpgradeAssistantMetrics should return correct info', async () => {
usageCollector.makeUpgradeAssistantUsageCollector(server());
registerUpgradeAssistantUsageCollector(usageCollection, server());
const upgradeAssistantStats = await makeUsageCollectorStub.mock.calls[0][0].fetch(
callClusterStub
);

View file

@ -7,6 +7,7 @@
import { set } from 'lodash';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { SavedObjectsRepository } from 'src/core/server/saved_objects/service/lib/repository';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import {
UPGRADE_ASSISTANT_DOC_ID,
UPGRADE_ASSISTANT_TYPE,
@ -97,12 +98,15 @@ export async function fetchUpgradeAssistantMetrics(
};
}
export function makeUpgradeAssistantUsageCollector(server: ServerShim) {
const upgradeAssistantUsageCollector = server.usage.collectorSet.makeUsageCollector({
export function registerUpgradeAssistantUsageCollector(
usageCollection: UsageCollectionSetup,
server: ServerShim
) {
const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({
type: UPGRADE_ASSISTANT_TYPE,
isReady: () => true,
fetch: async (callCluster: any) => fetchUpgradeAssistantMetrics(callCluster, server),
});
server.usage.collectorSet.register(upgradeAssistantUsageCollector);
usageCollection.registerCollector(upgradeAssistantUsageCollector);
}

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Plugin, CoreSetup, CoreStart } from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { ServerShim, ServerShimWithRouter } from './types';
import { credentialStoreFactory } from './lib/reindexing/credential_store';
import { makeUpgradeAssistantUsageCollector } from './lib/telemetry';
import { registerUpgradeAssistantUsageCollector } from './lib/telemetry';
import { registerClusterCheckupRoutes } from './routes/cluster_checkup';
import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging';
import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/reindex_indices';
@ -14,7 +15,10 @@ import { registerReindexIndicesRoutes, registerReindexWorker } from './routes/re
import { registerTelemetryRoutes } from './routes/telemetry';
export class UpgradeAssistantServerPlugin implements Plugin<void, void, object, object> {
setup({ http }: CoreSetup, { __LEGACY }: { __LEGACY: ServerShim }) {
setup(
{ http }: CoreSetup,
{ __LEGACY, usageCollection }: { usageCollection: UsageCollectionSetup; __LEGACY: ServerShim }
) {
const router = http.createRouter();
const shimWithRouter: ServerShimWithRouter = { ...__LEGACY, router };
registerClusterCheckupRoutes(shimWithRouter);
@ -33,7 +37,7 @@ export class UpgradeAssistantServerPlugin implements Plugin<void, void, object,
// Bootstrap the needed routes and the collector for the telemetry
registerTelemetryRoutes(shimWithRouter);
makeUpgradeAssistantUsageCollector(__LEGACY);
registerUpgradeAssistantUsageCollector(usageCollection, __LEGACY);
}
start(core: CoreStart, plugins: any) {}

Some files were not shown because too many files have changed in this diff Show more