[Telemetry] Server-side Migration to NP (#60485)

* [Telemetry] Migration to NP

* Telemetry management advanced settings section + fix import paths + dropped support for injectVars

* Fix i18nrc paths for telemetry

* Move ui_metric mappings to NP registerType

* Fixed minor test tweaks

* Add README docs (#60443)

* Add missing translation

* Update the telemetryService config only when authenticated

* start method is not a promise anymore

* Fix mocha tests

* No need to JSON.stringify the API responses

* Catch handleOldSettings as we used to do

* Deal with the forbidden use case in the optIn API

* No need to provide the plugin name in the logger.get(). It is automatically scoped + one missing CallCluster vs. APICaller type replacement

* Add empty start method in README.md to show differences with the other approach

* Telemetry collection with X-Pack README

* Docs update

* Allow monitoring collector to send its own ES client

* All collections should provide their own ES client

* PR feedback

* i18n NITs from kibana-platform feedback

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Haro 2020-03-23 18:49:38 +00:00 committed by GitHub
parent d8d06e7343
commit 452193fdba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
149 changed files with 2161 additions and 1654 deletions

View file

@ -35,8 +35,8 @@
"server": "src/legacy/server",
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": [
"src/legacy/core_plugins/telemetry",
"src/plugins/telemetry"
"src/plugins/telemetry",
"src/plugins/telemetry_management_section"
],
"tileMap": "src/legacy/core_plugins/tile_map",
"timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"],

View file

@ -1,36 +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.
*/
export const mappings = {
application_usage_totals: {
properties: {
appId: { type: 'keyword' },
numberOfClicks: { type: 'long' },
minutesOnScreen: { type: 'float' },
},
},
application_usage_transactional: {
properties: {
timestamp: { type: 'date' },
appId: { type: 'keyword' },
numberOfClicks: { type: 'long' },
minutesOnScreen: { type: 'float' },
},
},
};

View file

@ -1,4 +0,0 @@
{
"name": "application_usage",
"version": "kibana"
}

View file

@ -1,83 +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 { i18n } from '@kbn/i18n';
/*
* config options opt into telemetry
* @type {string}
*/
export const CONFIG_TELEMETRY = 'telemetry:optIn';
/*
* config description for opting into telemetry
* @type {string}
*/
export const getConfigTelemetryDesc = () => {
return i18n.translate('telemetry.telemetryConfigDescription', {
defaultMessage:
'Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic.',
});
};
/**
* The amount of time, in milliseconds, to wait between reports when enabled.
*
* Currently 24 hours.
* @type {Number}
*/
export const REPORT_INTERVAL_MS = 86400000;
/**
* Link to the Elastic Telemetry privacy statement.
*/
export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`;
/**
* The type name used within the Monitoring index to publish localization stats.
* @type {string}
*/
export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization';
/**
* The type name used to publish telemetry plugin stats.
* @type {string}
*/
export const TELEMETRY_STATS_TYPE = 'telemetry';
/**
* UI metric usage type
* @type {string}
*/
export const UI_METRIC_USAGE_TYPE = 'ui_metric';
/**
* Application Usage type
*/
export const APPLICATION_USAGE_TYPE = 'application_usage';
/**
* Link to Advanced Settings.
*/
export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings';
/**
* The type name used within the Monitoring index to publish management stats.
* @type {string}
*/
export const KIBANA_STACK_MANAGEMENT_STATS_TYPE = 'stack_management';

View file

@ -1,153 +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 * as Rx from 'rxjs';
import { resolve } from 'path';
import JoiNamespace from 'joi';
import { Server } from 'hapi';
import { PluginInitializerContext } from 'src/core/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { getConfigPath } from '../../../core/server/path';
// @ts-ignore
import mappings from './mappings.json';
import {
telemetryPlugin,
replaceTelemetryInjectedVars,
FetcherTask,
PluginsSetup,
handleOldSettings,
} from './server';
const ENDPOINT_VERSION = 'v2';
const telemetry = (kibana: any) => {
return new kibana.Plugin({
id: 'telemetry',
configPrefix: 'telemetry',
publicDir: resolve(__dirname, 'public'),
require: ['elasticsearch'],
config(Joi: typeof JoiNamespace) {
return Joi.object({
enabled: Joi.boolean().default(true),
allowChangingOptInStatus: Joi.boolean().default(true),
optIn: Joi.when('allowChangingOptInStatus', {
is: false,
then: Joi.valid(true).default(true),
otherwise: Joi.boolean().default(true),
}),
// `config` is used internally and not intended to be set
config: Joi.string().default(getConfigPath()),
banner: Joi.boolean().default(true),
url: Joi.when('$dev', {
is: true,
then: Joi.string().default(
`https://telemetry-staging.elastic.co/xpack/${ENDPOINT_VERSION}/send`
),
otherwise: Joi.string().default(
`https://telemetry.elastic.co/xpack/${ENDPOINT_VERSION}/send`
),
}),
optInStatusUrl: Joi.when('$dev', {
is: true,
then: Joi.string().default(
`https://telemetry-staging.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`
),
otherwise: Joi.string().default(
`https://telemetry.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`
),
}),
sendUsageFrom: Joi.string()
.allow(['server', 'browser'])
.default('browser'),
}).default();
},
uiExports: {
managementSections: ['plugins/telemetry/views/management'],
savedObjectSchemas: {
telemetry: {
isNamespaceAgnostic: true,
},
},
async replaceInjectedVars(originalInjectedVars: any, request: any, server: any) {
const telemetryInjectedVars = await replaceTelemetryInjectedVars(request, server);
return Object.assign({}, originalInjectedVars, telemetryInjectedVars);
},
injectDefaultVars(server: Server) {
const config = server.config();
return {
telemetryEnabled: config.get('telemetry.enabled'),
telemetryUrl: config.get('telemetry.url'),
telemetryBanner:
config.get('telemetry.allowChangingOptInStatus') !== false &&
config.get('telemetry.banner'),
telemetryOptedIn: config.get('telemetry.optIn'),
telemetryOptInStatusUrl: config.get('telemetry.optInStatusUrl'),
allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'),
telemetrySendUsageFrom: config.get('telemetry.sendUsageFrom'),
telemetryNotifyUserAboutOptInDefault: false,
};
},
mappings,
},
postInit(server: Server) {
const fetcherTask = new FetcherTask(server);
fetcherTask.start();
},
async init(server: Server) {
const { usageCollection } = server.newPlatform.setup.plugins;
const initializerContext = {
env: {
packageInfo: {
version: server.config().get('pkg.version'),
},
},
config: {
create() {
const config = server.config();
return Rx.of({
enabled: config.get('telemetry.enabled'),
optIn: config.get('telemetry.optIn'),
config: config.get('telemetry.config'),
banner: config.get('telemetry.banner'),
url: config.get('telemetry.url'),
allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'),
});
},
},
} as PluginInitializerContext;
try {
await handleOldSettings(server);
} catch (err) {
server.log(['warning', 'telemetry'], 'Unable to update legacy telemetry configs.');
}
const pluginsSetup: PluginsSetup = {
usageCollection,
};
const npPlugin = telemetryPlugin(initializerContext);
await npPlugin.setup(server.newPlatform.setup.core, pluginsSetup, server);
await npPlugin.start(server.newPlatform.start.core);
},
});
};
// eslint-disable-next-line import/no-default-export
export default telemetry;

View file

@ -1,30 +0,0 @@
{
"telemetry": {
"properties": {
"enabled": {
"type": "boolean"
},
"sendUsageFrom": {
"ignore_above": 256,
"type": "keyword"
},
"lastReported": {
"type": "date"
},
"lastVersionChecked": {
"ignore_above": 256,
"type": "keyword"
},
"userHasSeenNotice": {
"type": "boolean"
},
"reportFailureCount": {
"type": "integer"
},
"reportFailureVersion": {
"ignore_above": 256,
"type": "keyword"
}
}
}
}

View file

@ -1,4 +0,0 @@
{
"name": "telemetry",
"version": "kibana"
}

View file

@ -1,49 +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 React from 'react';
import routes from 'ui/routes';
import { npStart, npSetup } from 'ui/new_platform';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { TelemetryManagementSection } from '../../../../../../plugins/telemetry/public/components';
routes.defaults(/\/management/, {
resolve: {
telemetryManagementSection() {
const { telemetry } = npStart.plugins as any;
const { advancedSettings } = npSetup.plugins as any;
if (telemetry && advancedSettings) {
const componentRegistry = advancedSettings.component;
const Component = (props: any) => (
<TelemetryManagementSection
showAppliesSettingMessage={true}
telemetryService={telemetry.telemetryService}
{...props}
/>
);
componentRegistry.register(
componentRegistry.componentType.PAGE_FOOTER_COMPONENT,
Component,
true
);
}
},
},
});

View file

@ -1,249 +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 { encryptTelemetry } from './collectors';
import { CallCluster } from '../../elasticsearch';
import { UsageCollectionSetup } from '../../../../plugins/usage_collection/server';
import { Cluster } from '../../elasticsearch';
import { ESLicense } from './telemetry_collection/get_local_license';
export type EncryptedStatsGetterConfig = { unencrypted: false } & {
server: any;
start: string;
end: string;
};
export type UnencryptedStatsGetterConfig = { unencrypted: true } & {
req: any;
start: string;
end: string;
};
export interface ClusterDetails {
clusterUuid: string;
}
export interface StatsCollectionConfig {
usageCollection: UsageCollectionSetup;
callCluster: CallCluster;
server: any;
start: string | number;
end: string | number;
}
export interface BasicStatsPayload {
timestamp: string;
cluster_uuid: string;
cluster_name: string;
version: string;
cluster_stats: object;
collection?: string;
stack_stats: object;
}
export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig;
export type ClusterDetailsGetter = (config: StatsCollectionConfig) => Promise<ClusterDetails[]>;
export type StatsGetter<T extends BasicStatsPayload = BasicStatsPayload> = (
clustersDetails: ClusterDetails[],
config: StatsCollectionConfig
) => Promise<T[]>;
export type LicenseGetter = (
clustersDetails: ClusterDetails[],
config: StatsCollectionConfig
) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>;
interface CollectionConfig<T extends BasicStatsPayload> {
title: string;
priority: number;
esCluster: string | Cluster;
statsGetter: StatsGetter<T>;
clusterDetailsGetter: ClusterDetailsGetter;
licenseGetter: LicenseGetter;
}
interface Collection {
statsGetter: StatsGetter;
licenseGetter: LicenseGetter;
clusterDetailsGetter: ClusterDetailsGetter;
esCluster: string | Cluster;
title: string;
}
export class TelemetryCollectionManager {
private usageGetterMethodPriority = -1;
private collections: Collection[] = [];
public setCollection = <T extends BasicStatsPayload>(collectionConfig: CollectionConfig<T>) => {
const {
title,
priority,
esCluster,
statsGetter,
clusterDetailsGetter,
licenseGetter,
} = collectionConfig;
if (typeof priority !== 'number') {
throw new Error('priority must be set.');
}
if (priority === this.usageGetterMethodPriority) {
throw new Error(`A Usage Getter with the same priority is already set.`);
}
if (priority > this.usageGetterMethodPriority) {
if (!statsGetter) {
throw Error('Stats getter method not set.');
}
if (!esCluster) {
throw Error('esCluster name must be set for the getCluster method.');
}
if (!clusterDetailsGetter) {
throw Error('Cluster UUIds method is not set.');
}
if (!licenseGetter) {
throw Error('License getter method not set.');
}
this.collections.unshift({
licenseGetter,
statsGetter,
clusterDetailsGetter,
esCluster,
title,
});
this.usageGetterMethodPriority = priority;
}
};
private getStatsCollectionConfig = async (
collection: Collection,
config: StatsGetterConfig
): Promise<StatsCollectionConfig> => {
const { start, end } = config;
const server = config.unencrypted ? config.req.server : config.server;
const { callWithRequest, callWithInternalUser } =
typeof collection.esCluster === 'string'
? server.plugins.elasticsearch.getCluster(collection.esCluster)
: collection.esCluster;
const callCluster = config.unencrypted
? (...args: any[]) => callWithRequest(config.req, ...args)
: callWithInternalUser;
const { usageCollection } = server.newPlatform.setup.plugins;
return { server, callCluster, start, end, usageCollection };
};
private getOptInStatsForCollection = async (
collection: Collection,
optInStatus: boolean,
statsCollectionConfig: StatsCollectionConfig
) => {
const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig);
return clustersDetails.map(({ clusterUuid }) => ({
cluster_uuid: clusterUuid,
opt_in_status: optInStatus,
}));
};
private getUsageForCollection = async (
collection: Collection,
statsCollectionConfig: StatsCollectionConfig
) => {
const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig);
if (clustersDetails.length === 0) {
// don't bother doing a further lookup, try next collection.
return;
}
const [stats, licenses] = await Promise.all([
collection.statsGetter(clustersDetails, statsCollectionConfig),
collection.licenseGetter(clustersDetails, statsCollectionConfig),
]);
return stats.map(stat => {
const license = licenses[stat.cluster_uuid];
return {
...(license ? { license } : {}),
...stat,
collectionSource: collection.title,
};
});
};
public getOptInStats = async (optInStatus: boolean, config: StatsGetterConfig) => {
for (const collection of this.collections) {
const statsCollectionConfig = await this.getStatsCollectionConfig(collection, config);
try {
const optInStats = await this.getOptInStatsForCollection(
collection,
optInStatus,
statsCollectionConfig
);
if (optInStats && optInStats.length) {
statsCollectionConfig.server.log(
['debug', 'telemetry', 'collection'],
`Got Opt In stats using ${collection.title} collection.`
);
if (config.unencrypted) {
return optInStats;
}
const isDev = statsCollectionConfig.server.config().get('env.dev');
return encryptTelemetry(optInStats, isDev);
}
} catch (err) {
statsCollectionConfig.server.log(
['debu', 'telemetry', 'collection'],
`Failed to collect any opt in stats with registered collections.`
);
// swallow error to try next collection;
}
}
return [];
};
public getStats = async (config: StatsGetterConfig) => {
for (const collection of this.collections) {
const statsCollectionConfig = await this.getStatsCollectionConfig(collection, config);
try {
const usageData = await this.getUsageForCollection(collection, statsCollectionConfig);
if (usageData && usageData.length) {
statsCollectionConfig.server.log(
['debug', 'telemetry', 'collection'],
`Got Usage using ${collection.title} collection.`
);
if (config.unencrypted) {
return usageData;
}
const isDev = statsCollectionConfig.server.config().get('env.dev');
return encryptTelemetry(usageData, isDev);
}
} catch (err) {
statsCollectionConfig.server.log(
['debug', 'telemetry', 'collection'],
`Failed to collect any usage with registered collections.`
);
// swallow error to try next collection;
}
}
return [];
};
}
export const telemetryCollectionManager = new TelemetryCollectionManager();

View file

@ -1,70 +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 {
CoreSetup,
PluginInitializerContext,
ISavedObjectsRepository,
CoreStart,
} 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,
registerManagementUsageCollector,
registerApplicationUsageCollector,
} from './collectors';
export interface PluginsSetup {
usageCollection: UsageCollectionSetup;
}
export class TelemetryPlugin {
private readonly currentKibanaVersion: string;
private savedObjectsClient?: ISavedObjectsRepository;
constructor(initializerContext: PluginInitializerContext) {
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
}
public setup(core: CoreSetup, { usageCollection }: PluginsSetup, server: Server) {
const currentKibanaVersion = this.currentKibanaVersion;
registerCollection();
registerRoutes({ core, currentKibanaVersion, server });
const getSavedObjectsClient = () => this.savedObjectsClient;
registerTelemetryPluginUsageCollector(usageCollection, server);
registerLocalizationUsageCollector(usageCollection, server);
registerTelemetryUsageCollector(usageCollection, server);
registerUiMetricUsageCollector(usageCollection, getSavedObjectsClient);
registerManagementUsageCollector(usageCollection, server);
registerApplicationUsageCollector(usageCollection, getSavedObjectsClient);
}
public start({ savedObjects }: CoreStart) {
this.savedObjectsClient = savedObjects.createInternalRepository();
}
}

View file

@ -1,97 +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 Joi from 'joi';
import moment from 'moment';
import { boomify } from 'boom';
import { CoreSetup } from 'src/core/server';
import { Legacy } from 'kibana';
import { getTelemetryAllowChangingOptInStatus } from '../telemetry_config';
import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats';
import {
TelemetrySavedObjectAttributes,
updateTelemetrySavedObject,
} from '../telemetry_repository';
interface RegisterOptInRoutesParams {
core: CoreSetup;
currentKibanaVersion: string;
server: Legacy.Server;
}
export function registerTelemetryOptInRoutes({
server,
currentKibanaVersion,
}: RegisterOptInRoutesParams) {
server.route({
method: 'POST',
path: '/api/telemetry/v2/optIn',
options: {
validate: {
payload: Joi.object({
enabled: Joi.bool().required(),
}),
},
},
handler: async (req: any, h: any) => {
try {
const newOptInStatus = req.payload.enabled;
const attributes: TelemetrySavedObjectAttributes = {
enabled: newOptInStatus,
lastVersionChecked: currentKibanaVersion,
};
const config = req.server.config();
const savedObjectsClient = req.getSavedObjectsClient();
const configTelemetryAllowChangingOptInStatus = config.get(
'telemetry.allowChangingOptInStatus'
);
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
telemetrySavedObject: savedObjectsClient,
configTelemetryAllowChangingOptInStatus,
});
if (!allowChangingOptInStatus) {
return h.response({ error: 'Not allowed to change Opt-in Status.' }).code(400);
}
const sendUsageFrom = config.get('telemetry.sendUsageFrom');
if (sendUsageFrom === 'server') {
const optInStatusUrl = config.get('telemetry.optInStatusUrl');
await sendTelemetryOptInStatus(
{ optInStatusUrl, newOptInStatus },
{
start: moment()
.subtract(20, 'minutes')
.toISOString(),
end: moment().toISOString(),
server: req.server,
unencrypted: false,
}
);
}
await updateTelemetrySavedObject(savedObjectsClient, attributes);
return h.response({}).code(200);
} catch (err) {
return boomify(err);
}
},
});
}

View file

@ -1,67 +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 Joi from 'joi';
import { boomify } from 'boom';
import { Legacy } from 'kibana';
import { telemetryCollectionManager } from '../collection_manager';
export function registerTelemetryUsageStatsRoutes(server: Legacy.Server) {
server.route({
method: 'POST',
path: '/api/telemetry/v2/clusters/_stats',
options: {
validate: {
payload: Joi.object({
unencrypted: Joi.bool(),
timeRange: Joi.object({
min: Joi.date().required(),
max: Joi.date().required(),
}).required(),
}),
},
},
handler: async (req: any, h: any) => {
const config = req.server.config();
const start = req.payload.timeRange.min;
const end = req.payload.timeRange.max;
const unencrypted = req.payload.unencrypted;
try {
return await telemetryCollectionManager.getStats({
unencrypted,
server,
req,
start,
end,
});
} catch (err) {
const isDev = config.get('env.dev');
if (isDev) {
// don't ignore errors when running in dev mode
return boomify(err, { statusCode: err.status || 500 });
} else {
const statusCode = unencrypted && err.status === 403 ? 403 : 200;
// ignore errors and return empty set
return h.response([]).code(statusCode);
}
}
},
});
}

View file

@ -1,72 +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 { getTelemetrySavedObject } from '../telemetry_repository';
import { getTelemetryOptIn } from './get_telemetry_opt_in';
import { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
import { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
import { getNotifyUserAboutOptInDefault } from './get_telemetry_notify_user_about_optin_default';
export async function replaceTelemetryInjectedVars(request: any, server: any) {
const config = server.config();
const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom');
const configTelemetryOptIn = config.get('telemetry.optIn');
const configTelemetryAllowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
const isRequestingApplication = request.path.startsWith('/app');
// Prevent interstitial screens (such as the space selector) from prompting for telemetry
if (!isRequestingApplication) {
return {
telemetryOptedIn: false,
};
}
const currentKibanaVersion = config.get('pkg.version');
const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient(request);
const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsClient);
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus,
telemetrySavedObject,
});
const telemetryOptedIn = getTelemetryOptIn({
configTelemetryOptIn,
allowChangingOptInStatus,
telemetrySavedObject,
currentKibanaVersion,
});
const telemetrySendUsageFrom = getTelemetrySendUsageFrom({
configTelemetrySendUsageFrom,
telemetrySavedObject,
});
const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn,
telemetryOptedIn,
});
return {
telemetryOptedIn,
telemetrySendUsageFrom,
telemetryNotifyUserAboutOptInDefault,
};
}

View file

@ -25,9 +25,6 @@ export default function(kibana: any) {
id: 'ui_metric',
require: ['kibana', 'elasticsearch'],
publicDir: resolve(__dirname, 'public'),
uiExports: {
mappings: require('./mappings.json'),
},
init() {},
});
}

View file

@ -1,9 +0,0 @@
{
"ui-metric": {
"properties": {
"count": {
"type": "integer"
}
}
}
}

View file

@ -17,15 +17,9 @@
* under the License.
*/
import { Legacy } from '../../../../kibana';
import { mappings } from './mappings';
export const I18N_RC = '.i18nrc.json';
// eslint-disable-next-line import/no-default-export
export default function ApplicationUsagePlugin(kibana: any) {
const config: Legacy.PluginSpecOptions = {
id: 'application_usage',
uiExports: { mappings }, // Needed to define the mappings for the SavedObjects
};
return new kibana.Plugin(config);
}
/**
* The type name used within the Monitoring index to publish localization stats.
*/
export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization';

View file

@ -24,16 +24,20 @@ import globby from 'globby';
const readFileAsync = promisify(readFile);
export async function getTranslationPaths({ cwd, glob }) {
interface I18NRCFileStructure {
translations?: string[];
}
export async function getTranslationPaths({ cwd, glob }: { cwd: string; glob: string }) {
const entries = await globby(glob, { cwd });
const translationPaths = [];
const translationPaths: string[] = [];
for (const entry of entries) {
const entryFullPath = resolve(cwd, entry);
const pluginBasePath = dirname(entryFullPath);
try {
const content = await readFileAsync(entryFullPath, 'utf8');
const { translations } = JSON.parse(content);
const { translations } = JSON.parse(content) as I18NRCFileStructure;
if (translations && translations.length) {
translations.forEach(translation => {
const translationFullPath = resolve(pluginBasePath, translation);

View file

@ -19,30 +19,35 @@
import { i18n, i18nLoader } from '@kbn/i18n';
import { basename } from 'path';
import { Server } from 'hapi';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { fromRoot } from '../../../core/server/utils';
import { getTranslationPaths } from './get_translations_path';
import { I18N_RC } from './constants';
import KbnServer, { KibanaConfig } from '../kbn_server';
import { registerLocalizationUsageCollector } from './localization';
export async function i18nMixin(kbnServer, server, config) {
const locale = config.get('i18n.locale');
export async function i18nMixin(kbnServer: KbnServer, server: Server, config: KibanaConfig) {
const locale = config.get('i18n.locale') as string;
const translationPaths = await Promise.all([
getTranslationPaths({
cwd: fromRoot('.'),
glob: I18N_RC,
}),
...config.get('plugins.paths').map(cwd => getTranslationPaths({ cwd, glob: I18N_RC })),
...config
.get('plugins.scanDirs')
.map(cwd => getTranslationPaths({ cwd, glob: `*/${I18N_RC}` })),
...(config.get('plugins.paths') as string[]).map(cwd =>
getTranslationPaths({ cwd, glob: I18N_RC })
),
...(config.get('plugins.scanDirs') as string[]).map(cwd =>
getTranslationPaths({ cwd, glob: `*/${I18N_RC}` })
),
getTranslationPaths({
cwd: fromRoot('../kibana-extra'),
glob: `*/${I18N_RC}`,
}),
]);
const currentTranslationPaths = []
const currentTranslationPaths = ([] as string[])
.concat(...translationPaths)
.filter(translationPath => basename(translationPath, '.json') === locale);
i18nLoader.registerTranslationFiles(currentTranslationPaths);
@ -55,5 +60,14 @@ export async function i18nMixin(kbnServer, server, config) {
})
);
server.decorate('server', 'getTranslationsFilePaths', () => currentTranslationPaths);
const getTranslationsFilePaths = () => currentTranslationPaths;
server.decorate('server', 'getTranslationsFilePaths', getTranslationsFilePaths);
if (kbnServer.newPlatform.setup.plugins.usageCollection) {
registerLocalizationUsageCollector(kbnServer.newPlatform.setup.plugins.usageCollection, {
getLocale: () => config.get('i18n.locale') as string,
getTranslationsFilePaths,
});
}
}

View file

@ -22,16 +22,17 @@ interface TranslationsMock {
}
const createI18nLoaderMock = (translations: TranslationsMock) => {
return {
return ({
getTranslationsByLocale() {
return {
messages: translations,
};
},
};
} as unknown) as typeof i18nLoader;
};
import { getTranslationCount } from './telemetry_localization_collector';
import { i18nLoader } from '@kbn/i18n';
describe('getTranslationCount', () => {
it('returns 0 if no translations registered', async () => {

View file

@ -19,25 +19,36 @@
import { i18nLoader } from '@kbn/i18n';
import { size } from 'lodash';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getIntegrityHashes, Integrities } from './file_integrity';
import { KIBANA_LOCALIZATION_STATS_TYPE } from '../../../common/constants';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
import { KIBANA_LOCALIZATION_STATS_TYPE } from '../constants';
export interface UsageStats {
locale: string;
integrities: Integrities;
labelsCount?: number;
}
export async function getTranslationCount(loader: any, locale: string): Promise<number> {
export interface LocalizationUsageCollectorHelpers {
getLocale: () => string;
getTranslationsFilePaths: () => string[];
}
export async function getTranslationCount(
loader: typeof i18nLoader,
locale: string
): Promise<number> {
const translations = await loader.getTranslationsByLocale(locale);
return size(translations.messages);
}
export function createCollectorFetch(server: any) {
export function createCollectorFetch({
getLocale,
getTranslationsFilePaths,
}: LocalizationUsageCollectorHelpers) {
return async function fetchUsageStats(): Promise<UsageStats> {
const config = server.config();
const locale: string = config.get('i18n.locale');
const translationFilePaths: string[] = server.getTranslationsFilePaths();
const locale = getLocale();
const translationFilePaths: string[] = getTranslationsFilePaths();
const [labelsCount, integrities] = await Promise.all([
getTranslationCount(i18nLoader, locale),
@ -54,12 +65,12 @@ export function createCollectorFetch(server: any) {
export function registerLocalizationUsageCollector(
usageCollection: UsageCollectionSetup,
server: any
helpers: LocalizationUsageCollectorHelpers
) {
const collector = usageCollection.makeUsageCollector({
type: KIBANA_LOCALIZATION_STATS_TYPE,
isReady: () => true,
fetch: createCollectorFetch(server),
fetch: createCollectorFetch(helpers),
});
usageCollection.registerCollector(collector);

View file

@ -20,6 +20,7 @@
import { ResponseObject, Server } from 'hapi';
import { UnwrapPromise } from '@kbn/utility-types';
import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server';
import {
ConfigService,
CoreSetup,
@ -104,6 +105,7 @@ type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promi
export interface PluginsSetup {
usageCollection: UsageCollectionSetup;
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup;
home: HomeServerPluginSetup;
[key: string]: object;
}

View file

@ -7,7 +7,6 @@
import { APICaller as APICaller_2 } from 'kibana/server';
import Boom from 'boom';
import { BulkIndexDocumentsParams } from 'elasticsearch';
import { CallCluster as CallCluster_2 } from 'src/legacy/core_plugins/elasticsearch';
import { CatAliasesParams } from 'elasticsearch';
import { CatAllocationParams } from 'elasticsearch';
import { CatCommonParams } from 'elasticsearch';

View file

@ -6,4 +6,4 @@ Telemetry allows Kibana features to have usage tracked in the wild. The general
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
This plugin is responsible for sending usage data to the telemetry cluster. For collecting usage data, use the [`usageCollection` plugin](../usage_collection/README.md)

View file

@ -17,13 +17,31 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
/**
* config options opt into telemetry
*/
export const CONFIG_TELEMETRY = 'telemetry:optIn';
/**
* config description for opting into telemetry
*/
export const getConfigTelemetryDesc = () => {
// Can't find where it's used but copying it over from the legacy code just in case...
return i18n.translate('telemetry.telemetryConfigDescription', {
defaultMessage:
'Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic.',
});
};
/**
* The amount of time, in milliseconds, to wait between reports when enabled.
* Currently 24 hours.
*/
export const REPORT_INTERVAL_MS = 86400000;
/*
/**
* Key for the localStorage service
*/
export const LOCALSTORAGE_KEY = 'telemetry.data';
@ -37,3 +55,28 @@ export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings';
* Link to the Elastic Telemetry privacy statement.
*/
export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`;
/**
* The type name used to publish telemetry plugin stats.
*/
export const TELEMETRY_STATS_TYPE = 'telemetry';
/**
* The endpoint version when hitting the remote telemetry service
*/
export const ENDPOINT_VERSION = 'v2';
/**
* UI metric usage type
*/
export const UI_METRIC_USAGE_TYPE = 'ui_metric';
/**
* Application Usage type
*/
export const APPLICATION_USAGE_TYPE = 'application_usage';
/**
* The type name used within the Monitoring index to publish management stats.
*/
export const KIBANA_STACK_MANAGEMENT_STATS_TYPE = 'stack_management';

View file

@ -16,7 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
import { TelemetrySavedObject } from './types';
interface GetTelemetryAllowChangingOptInStatus {
configTelemetryAllowChangingOptInStatus: boolean;

View file

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
import { TelemetrySavedObject } from './types';
interface GetTelemetryFailureDetailsConfig {
telemetrySavedObject: TelemetrySavedObject;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
import { TelemetrySavedObject } from './types';
interface NotifyOpts {
allowChangingOptInStatus: boolean;

View file

@ -18,7 +18,7 @@
*/
import { getTelemetryOptIn } from './get_telemetry_opt_in';
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
import { TelemetrySavedObject } from './types';
describe('getTelemetryOptIn', () => {
it('returns null when saved object not found', () => {

View file

@ -18,7 +18,7 @@
*/
import semver from 'semver';
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
import { TelemetrySavedObject } from './types';
interface GetTelemetryOptInConfig {
telemetrySavedObject: TelemetrySavedObject;

View file

@ -18,7 +18,7 @@
*/
import { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
import { TelemetrySavedObject } from './types';
describe('getTelemetrySendUsageFrom', () => {
it('returns kibana.yml config when saved object not found', () => {

View file

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
import { TelemetrySavedObject } from './types';
interface GetTelemetryUsageFetcherConfig {
configTelemetrySendUsageFrom: 'browser' | 'server';

View file

@ -17,7 +17,6 @@
* under the License.
*/
export { replaceTelemetryInjectedVars } from './replace_injected_vars';
export { getTelemetryOptIn } from './get_telemetry_opt_in';
export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';

View file

@ -17,9 +17,6 @@
* under the License.
*/
export { getTelemetrySavedObject, TelemetrySavedObject } from './get_telemetry_saved_object';
export { updateTelemetrySavedObject } from './update_telemetry_saved_object';
export interface TelemetrySavedObjectAttributes {
enabled?: boolean | null;
lastVersionChecked?: string;
@ -30,3 +27,5 @@ export interface TelemetrySavedObjectAttributes {
reportFailureCount?: number;
reportFailureVersion?: string;
}
export type TelemetrySavedObject = TelemetrySavedObjectAttributes | null | false;

View file

@ -1,6 +1,10 @@
{
"id": "telemetry",
"version": "kibana",
"server": false,
"ui": true
"server": true,
"ui": true,
"requiredPlugins": [
"telemetryCollectionManager",
"usageCollection"
]
}

View file

@ -17,6 +17,4 @@
* under the License.
*/
export { OptInExampleFlyout } from './opt_in_example_flyout';
export { TelemetryManagementSection } from './telemetry_management_section';
export { OptedInNoticeBanner } from './opted_in_notice_banner';

View file

@ -17,9 +17,10 @@
* under the License.
*/
import { TelemetryPlugin } from './plugin';
import { PluginInitializerContext } from 'kibana/public';
import { TelemetryPlugin, TelemetryPluginConfig } from './plugin';
export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
export function plugin() {
return new TelemetryPlugin();
export function plugin(initializerContext: PluginInitializerContext<TelemetryPluginConfig>) {
return new TelemetryPlugin(initializerContext);
}

View file

@ -23,8 +23,6 @@ import { overlayServiceMock } from '../../../core/public/overlays/overlay_servic
import { httpServiceMock } from '../../../core/public/http/http_service.mock';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { injectedMetadataServiceMock } from '../../../core/public/injected_metadata/injected_metadata_service.mock';
import { TelemetryService } from './services/telemetry_service';
import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications';
import { TelemetryPluginStart } from './plugin';
@ -32,23 +30,19 @@ import { TelemetryPluginStart } from './plugin';
export function mockTelemetryService({
reportOptInStatusChange,
}: { reportOptInStatusChange?: boolean } = {}) {
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getInjectedVar.mockImplementation((key: string) => {
switch (key) {
case 'telemetryNotifyUserAboutOptInDefault':
return true;
case 'allowChangingOptInStatus':
return true;
case 'telemetryOptedIn':
return true;
default: {
throw Error(`Unhandled getInjectedVar key "${key}".`);
}
}
});
const config = {
enabled: true,
url: 'http://localhost',
optInStatusUrl: 'http://localhost',
sendUsageFrom: 'browser' as const,
optIn: true,
banner: true,
allowChangingOptInStatus: true,
telemetryNotifyUserAboutOptInDefault: true,
};
return new TelemetryService({
injectedMetadata,
config,
http: httpServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
reportOptInStatusChange,

View file

@ -16,9 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Plugin, CoreStart, CoreSetup, HttpStart } from '../../../core/public';
import {
Plugin,
CoreStart,
CoreSetup,
HttpStart,
PluginInitializerContext,
SavedObjectsClientContract,
} from '../../../core/public';
import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services';
import {
TelemetrySavedObjectAttributes,
TelemetrySavedObject,
} from '../common/telemetry_config/types';
import {
getTelemetryAllowChangingOptInStatus,
getTelemetryOptIn,
getTelemetrySendUsageFrom,
} from '../common/telemetry_config';
import { getNotifyUserAboutOptInDefault } from '../common/telemetry_config/get_telemetry_notify_user_about_optin_default';
export interface TelemetryPluginSetup {
telemetryService: TelemetryService;
@ -29,17 +47,32 @@ export interface TelemetryPluginStart {
telemetryNotifications: TelemetryNotifications;
}
export interface TelemetryPluginConfig {
enabled: boolean;
url: string;
banner: boolean;
allowChangingOptInStatus: boolean;
optIn: boolean | null;
optInStatusUrl: string;
sendUsageFrom: 'browser' | 'server';
telemetryNotifyUserAboutOptInDefault?: boolean;
}
export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPluginStart> {
private readonly currentKibanaVersion: string;
private readonly config: TelemetryPluginConfig;
private telemetrySender?: TelemetrySender;
private telemetryNotifications?: TelemetryNotifications;
private telemetryService?: TelemetryService;
public setup({ http, injectedMetadata, notifications }: CoreSetup): TelemetryPluginSetup {
this.telemetryService = new TelemetryService({
http,
injectedMetadata,
notifications,
});
constructor(initializerContext: PluginInitializerContext<TelemetryPluginConfig>) {
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
this.config = initializerContext.config.get();
}
public setup({ http, notifications }: CoreSetup): TelemetryPluginSetup {
const config = this.config;
this.telemetryService = new TelemetryService({ config, http, notifications });
this.telemetrySender = new TelemetrySender(this.telemetryService);
@ -48,24 +81,29 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
};
}
public start({ injectedMetadata, http, overlays, application }: CoreStart): TelemetryPluginStart {
public start({ http, overlays, application, savedObjects }: CoreStart): TelemetryPluginStart {
if (!this.telemetryService) {
throw Error('Telemetry plugin failed to initialize properly.');
}
const telemetryBanner = injectedMetadata.getInjectedVar('telemetryBanner') as boolean;
this.telemetryNotifications = new TelemetryNotifications({
overlays,
telemetryService: this.telemetryService,
});
application.currentAppId$.subscribe(appId => {
application.currentAppId$.subscribe(async () => {
const isUnauthenticated = this.getIsUnauthenticated(http);
if (isUnauthenticated) {
return;
}
// Update the telemetry config based as a mix of the config files and saved objects
const telemetrySavedObject = await this.getTelemetrySavedObject(savedObjects.client);
const updatedConfig = await this.updateConfigsBasedOnSavedObjects(telemetrySavedObject);
this.telemetryService!.config = updatedConfig;
const telemetryBanner = updatedConfig.banner;
this.maybeStartTelemetryPoller();
if (telemetryBanner) {
this.maybeShowOptedInNotificationBanner();
@ -111,4 +149,66 @@ export class TelemetryPlugin implements Plugin<TelemetryPluginSetup, TelemetryPl
this.telemetryNotifications.renderOptInBanner();
}
}
private async updateConfigsBasedOnSavedObjects(
telemetrySavedObject: TelemetrySavedObject
): Promise<TelemetryPluginConfig> {
const configTelemetrySendUsageFrom = this.config.sendUsageFrom;
const configTelemetryOptIn = this.config.optIn as boolean;
const configTelemetryAllowChangingOptInStatus = this.config.allowChangingOptInStatus;
const currentKibanaVersion = this.currentKibanaVersion;
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
configTelemetryAllowChangingOptInStatus,
telemetrySavedObject,
});
const optIn = getTelemetryOptIn({
configTelemetryOptIn,
allowChangingOptInStatus,
telemetrySavedObject,
currentKibanaVersion,
});
const sendUsageFrom = getTelemetrySendUsageFrom({
configTelemetrySendUsageFrom,
telemetrySavedObject,
});
const telemetryNotifyUserAboutOptInDefault = getNotifyUserAboutOptInDefault({
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn,
telemetryOptedIn: optIn,
});
return {
...this.config,
optIn,
sendUsageFrom,
telemetryNotifyUserAboutOptInDefault,
};
}
private async getTelemetrySavedObject(savedObjectsClient: SavedObjectsClientContract) {
try {
const { attributes } = await savedObjectsClient.get<TelemetrySavedObjectAttributes>(
'telemetry',
'telemetry'
);
return attributes;
} catch (error) {
const errorCode = error[Symbol('SavedObjectsClientErrorCode')];
if (errorCode === 'SavedObjectsClient/notFound') {
return null;
}
if (errorCode === 'SavedObjectsClient/forbidden') {
return false;
}
throw error;
}
}
}

View file

@ -20,62 +20,75 @@
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { CoreStart } from 'kibana/public';
import { TelemetryPluginConfig } from '../plugin';
interface TelemetryServiceConstructor {
config: TelemetryPluginConfig;
http: CoreStart['http'];
injectedMetadata: CoreStart['injectedMetadata'];
notifications: CoreStart['notifications'];
reportOptInStatusChange?: boolean;
}
export class TelemetryService {
private readonly http: CoreStart['http'];
private readonly injectedMetadata: CoreStart['injectedMetadata'];
private readonly reportOptInStatusChange: boolean;
private readonly notifications: CoreStart['notifications'];
private isOptedIn: boolean | null;
private userHasSeenOptedInNotice: boolean;
private readonly defaultConfig: TelemetryPluginConfig;
private updatedConfig?: TelemetryPluginConfig;
constructor({
config,
http,
injectedMetadata,
notifications,
reportOptInStatusChange = true,
}: TelemetryServiceConstructor) {
const isOptedIn = injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean | null;
const userHasSeenOptedInNotice = injectedMetadata.getInjectedVar(
'telemetryNotifyUserAboutOptInDefault'
) as boolean;
this.defaultConfig = config;
this.reportOptInStatusChange = reportOptInStatusChange;
this.injectedMetadata = injectedMetadata;
this.notifications = notifications;
this.http = http;
}
this.isOptedIn = isOptedIn;
this.userHasSeenOptedInNotice = userHasSeenOptedInNotice;
public set config(updatedConfig: TelemetryPluginConfig) {
this.updatedConfig = updatedConfig;
}
public get config() {
return { ...this.defaultConfig, ...this.updatedConfig };
}
public get isOptedIn() {
return this.config.optIn;
}
public set isOptedIn(optIn) {
this.config = { ...this.config, optIn };
}
public get userHasSeenOptedInNotice() {
return this.config.telemetryNotifyUserAboutOptInDefault;
}
public set userHasSeenOptedInNotice(telemetryNotifyUserAboutOptInDefault) {
this.config = { ...this.config, telemetryNotifyUserAboutOptInDefault };
}
public getCanChangeOptInStatus = () => {
const allowChangingOptInStatus = this.injectedMetadata.getInjectedVar(
'allowChangingOptInStatus'
) as boolean;
const allowChangingOptInStatus = this.config.allowChangingOptInStatus;
return allowChangingOptInStatus;
};
public getOptInStatusUrl = () => {
const telemetryOptInStatusUrl = this.injectedMetadata.getInjectedVar(
'telemetryOptInStatusUrl'
) as string;
const telemetryOptInStatusUrl = this.config.optInStatusUrl;
return telemetryOptInStatusUrl;
};
public getTelemetryUrl = () => {
const telemetryUrl = this.injectedMetadata.getInjectedVar('telemetryUrl') as string;
const telemetryUrl = this.config.url;
return telemetryUrl;
};
public getUserHasSeenOptedInNotice = () => {
return this.userHasSeenOptedInNotice;
return this.config.telemetryNotifyUserAboutOptInDefault || false;
};
public getIsOptedIn = () => {

View file

@ -17,10 +17,10 @@
* under the License.
*/
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
import { savedObjectsRepositoryMock } from '../../../../../../core/server/mocks';
import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/server';
import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { CollectorOptions } from '../../../../../../plugins/usage_collection/server/collector/collector';
import { CollectorOptions } from '../../../../../plugins/usage_collection/server/collector/collector';
import { registerApplicationUsageCollector } from './';
import {
@ -40,9 +40,12 @@ describe('telemetry_application_usage', () => {
} as any;
const getUsageCollector = jest.fn();
const registerType = jest.fn();
const callCluster = jest.fn();
beforeAll(() => registerApplicationUsageCollector(usageCollectionMock, getUsageCollector));
beforeAll(() =>
registerApplicationUsageCollector(usageCollectionMock, registerType, getUsageCollector)
);
afterAll(() => jest.clearAllTimers());
test('registered collector is set', () => {

View file

@ -0,0 +1,59 @@
/*
* 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 { SavedObjectAttributes, SavedObjectsServiceSetup } from 'kibana/server';
export interface ApplicationUsageTotal extends SavedObjectAttributes {
appId: string;
minutesOnScreen: number;
numberOfClicks: number;
}
export interface ApplicationUsageTransactional extends ApplicationUsageTotal {
timestamp: string;
}
export function registerMappings(registerType: SavedObjectsServiceSetup['registerType']) {
registerType({
name: 'application_usage_totals',
hidden: false,
namespaceAgnostic: true,
mappings: {
properties: {
appId: { type: 'keyword' },
numberOfClicks: { type: 'long' },
minutesOnScreen: { type: 'float' },
},
},
});
registerType({
name: 'application_usage_transactional',
hidden: false,
namespaceAgnostic: true,
mappings: {
properties: {
timestamp: { type: 'date' },
appId: { type: 'keyword' },
numberOfClicks: { type: 'long' },
minutesOnScreen: { type: 'float' },
},
},
});
}

View file

@ -18,10 +18,15 @@
*/
import moment from 'moment';
import { ISavedObjectsRepository, SavedObjectsServiceSetup } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { APPLICATION_USAGE_TYPE } from '../../../common/constants';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
import { ISavedObjectsRepository, SavedObjectAttributes } from '../../../../../../core/server';
import { findAll } from '../find_all';
import {
ApplicationUsageTotal,
ApplicationUsageTransactional,
registerMappings,
} from './saved_objects_types';
/**
* Roll indices every 24h
@ -36,16 +41,6 @@ export const ROLL_INDICES_START = 5 * 60 * 1000;
export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals';
export const SAVED_OBJECTS_TRANSACTIONAL_TYPE = 'application_usage_transactional';
interface ApplicationUsageTotal extends SavedObjectAttributes {
appId: string;
minutesOnScreen: number;
numberOfClicks: number;
}
interface ApplicationUsageTransactional extends ApplicationUsageTotal {
timestamp: string;
}
interface ApplicationUsageTelemetryReport {
[appId: string]: {
clicks_total: number;
@ -61,8 +56,11 @@ interface ApplicationUsageTelemetryReport {
export function registerApplicationUsageCollector(
usageCollection: UsageCollectionSetup,
registerType: SavedObjectsServiceSetup['registerType'],
getSavedObjectsClient: () => ISavedObjectsRepository | undefined
) {
registerMappings(registerType);
const collector = usageCollection.makeUsageCollector({
type: APPLICATION_USAGE_TYPE,
isReady: () => typeof getSavedObjectsClient() !== 'undefined',

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks';
import { savedObjectsRepositoryMock } from '../../../../core/server/mocks';
import { findAll } from './find_all';

View file

@ -17,10 +17,8 @@
* under the License.
*/
export { encryptTelemetry } from './encryption';
export { registerTelemetryUsageCollector } from './usage';
export { registerUiMetricUsageCollector } from './ui_metric';
export { registerLocalizationUsageCollector } from './localization';
export { registerTelemetryPluginUsageCollector } from './telemetry_plugin';
export { registerManagementUsageCollector } from './management';
export { registerApplicationUsageCollector } from './application_usage';

View file

@ -17,11 +17,10 @@
* under the License.
*/
import { Server } from 'hapi';
import { size } from 'lodash';
import { IUiSettingsClient } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { KIBANA_STACK_MANAGEMENT_STATS_TYPE } from '../../../common/constants';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
import { SavedObjectsClient } from '../../../../../../core/server';
export type UsageStats = Record<string, any>;
@ -30,12 +29,12 @@ export async function getTranslationCount(loader: any, locale: string): Promise<
return size(translations.messages);
}
export function createCollectorFetch(server: Server) {
return async function fetchUsageStats(): Promise<UsageStats> {
const internalRepo = server.newPlatform.start.core.savedObjects.createInternalRepository();
const uiSettingsClient = server.newPlatform.start.core.uiSettings.asScopedToClient(
new SavedObjectsClient(internalRepo)
);
export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClient | undefined) {
return async function fetchUsageStats(): Promise<UsageStats | undefined> {
const uiSettingsClient = getUiSettingsClient();
if (!uiSettingsClient) {
return;
}
const user = await uiSettingsClient.getUserProvided();
const modifiedEntries = Object.keys(user)
@ -51,12 +50,12 @@ export function createCollectorFetch(server: Server) {
export function registerManagementUsageCollector(
usageCollection: UsageCollectionSetup,
server: any
getUiSettingsClient: () => IUiSettingsClient | undefined
) {
const collector = usageCollection.makeUsageCollector({
type: KIBANA_STACK_MANAGEMENT_STATS_TYPE,
isReady: () => true,
fetch: createCollectorFetch(server),
isReady: () => typeof getUiSettingsClient() !== 'undefined',
fetch: createCollectorFetch(getUiSettingsClient),
});
usageCollection.registerCollector(collector);

View file

@ -17,10 +17,14 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { ISavedObjectsRepository, SavedObjectsClient } from '../../../../../core/server';
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';
import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../../common/telemetry_config';
import { UsageCollectionSetup } from '../../../../usage_collection/server';
import { TelemetryConfigType } from '../../config';
export interface TelemetryUsageStats {
opt_in_status?: boolean | null;
@ -28,21 +32,31 @@ export interface TelemetryUsageStats {
last_reported?: number;
}
export function createCollectorFetch(server: any) {
export interface TelemetryPluginUsageCollectorOptions {
currentKibanaVersion: string;
config$: Observable<TelemetryConfigType>;
getSavedObjectsClient: () => ISavedObjectsRepository | undefined;
}
export function createCollectorFetch({
currentKibanaVersion,
config$,
getSavedObjectsClient,
}: TelemetryPluginUsageCollectorOptions) {
return async function fetchUsageStats(): Promise<TelemetryUsageStats> {
const config = server.config();
const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom');
const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
const configTelemetryOptIn = config.get('telemetry.optIn');
const currentKibanaVersion = config.get('pkg.version');
const { sendUsageFrom, allowChangingOptInStatus, optIn = null } = await config$
.pipe(take(1))
.toPromise();
const configTelemetrySendUsageFrom = sendUsageFrom;
const configTelemetryOptIn = optIn;
let telemetrySavedObject: TelemetrySavedObject = {};
try {
const { getSavedObjectsRepository } = server.savedObjects;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
telemetrySavedObject = await getTelemetrySavedObject(internalRepository);
const internalRepository = getSavedObjectsClient()!;
telemetrySavedObject = await getTelemetrySavedObject(
new SavedObjectsClient(internalRepository)
);
} catch (err) {
// no-op
}
@ -65,12 +79,12 @@ export function createCollectorFetch(server: any) {
export function registerTelemetryPluginUsageCollector(
usageCollection: UsageCollectionSetup,
server: any
options: TelemetryPluginUsageCollectorOptions
) {
const collector = usageCollection.makeUsageCollector({
type: TELEMETRY_STATS_TYPE,
isReady: () => true,
fetch: createCollectorFetch(server),
isReady: () => typeof options.getSavedObjectsClient() !== 'undefined',
fetch: createCollectorFetch(options),
});
usageCollection.registerCollector(collector);

View file

@ -17,10 +17,10 @@
* under the License.
*/
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
import { savedObjectsRepositoryMock } from '../../../../../../core/server/mocks';
import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/server';
import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { CollectorOptions } from '../../../../../../plugins/usage_collection/server/collector/collector';
import { CollectorOptions } from '../../../../../plugins/usage_collection/server/collector/collector';
import { registerUiMetricUsageCollector } from './';
@ -33,9 +33,12 @@ describe('telemetry_ui_metric', () => {
} as any;
const getUsageCollector = jest.fn();
const registerType = jest.fn();
const callCluster = jest.fn();
beforeAll(() => registerUiMetricUsageCollector(usageCollectionMock, getUsageCollector));
beforeAll(() =>
registerUiMetricUsageCollector(usageCollectionMock, registerType, getUsageCollector)
);
test('registered collector is set', () => {
expect(collector).not.toBeUndefined();

View file

@ -17,9 +17,13 @@
* under the License.
*/
import { ISavedObjectsRepository, SavedObjectAttributes } from 'kibana/server';
import {
ISavedObjectsRepository,
SavedObjectAttributes,
SavedObjectsServiceSetup,
} from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { UI_METRIC_USAGE_TYPE } from '../../../common/constants';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
import { findAll } from '../find_all';
interface UIMetricsSavedObjects extends SavedObjectAttributes {
@ -28,8 +32,22 @@ interface UIMetricsSavedObjects extends SavedObjectAttributes {
export function registerUiMetricUsageCollector(
usageCollection: UsageCollectionSetup,
registerType: SavedObjectsServiceSetup['registerType'],
getSavedObjectsClient: () => ISavedObjectsRepository | undefined
) {
registerType({
name: 'ui-metric',
hidden: false,
namespaceAgnostic: true,
mappings: {
properties: {
count: {
type: 'integer',
},
},
},
});
const collector = usageCollection.makeUsageCollector({
type: UI_METRIC_USAGE_TYPE,
fetch: async () => {

View file

@ -18,7 +18,6 @@
*/
import { writeFileSync, unlinkSync } from 'fs';
import { Server } from 'hapi';
import { resolve } from 'path';
import { tmpdir } from 'os';
import {
@ -32,20 +31,6 @@ const mockUsageCollector = () => ({
makeUsageCollector: jest.fn().mockImplementationOnce((arg: object) => arg),
});
const serverWithConfig = (configPath: string): Server => {
return {
config: () => ({
get: (key: string) => {
if (key !== 'telemetry.config' && key !== 'xpack.xpack_main.telemetry.config') {
throw new Error('Expected `telemetry.config`');
}
return configPath;
},
}),
} as Server;
};
describe('telemetry_usage_collector', () => {
const tempDir = tmpdir();
const tempFiles = {
@ -129,11 +114,13 @@ describe('telemetry_usage_collector', () => {
// 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: Server = serverWithConfig(tempFiles.unreadable);
// the `makeUsageCollector` is mocked above to return the argument passed to it
const usageCollector = mockUsageCollector() as any;
const collectorOptions = createTelemetryUsageCollector(usageCollector, server);
const collectorOptions = createTelemetryUsageCollector(
usageCollector,
() => tempFiles.unreadable
);
expect(collectorOptions.type).toBe('static_telemetry');
expect(await collectorOptions.fetch({} as any)).toEqual(expectedObject); // Sending any as the callCluster client because it's not needed in this collector but TS requires it when calling it.

View file

@ -18,13 +18,16 @@
*/
import { accessSync, constants, readFileSync, statSync } from 'fs';
import { Server } from 'hapi';
import { safeLoad } from 'js-yaml';
import { dirname, join } from 'path';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { getConfigPath } from '../../../../../core/server/path';
// look for telemetry.yml in the same places we expect kibana.yml
import { ensureDeepObject } from './ensure_deep_object';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
/**
* The maximum file size before we ignore it (note: this limit is arbitrary).
@ -77,24 +80,20 @@ export async function readTelemetryFile(path: string): Promise<object | undefine
export function createTelemetryUsageCollector(
usageCollection: UsageCollectionSetup,
server: Server
getConfigPathFn = getConfigPath // exposed for testing
) {
return usageCollection.makeUsageCollector({
type: 'static_telemetry',
isReady: () => true,
fetch: async () => {
const config = server.config();
const configPath = config.get('telemetry.config') as string;
const configPath = getConfigPathFn();
const telemetryPath = join(dirname(configPath), 'telemetry.yml');
return await readTelemetryFile(telemetryPath);
},
});
}
export function registerTelemetryUsageCollector(
usageCollection: UsageCollectionSetup,
server: Server
) {
const collector = createTelemetryUsageCollector(usageCollection, server);
export function registerTelemetryUsageCollector(usageCollection: UsageCollectionSetup) {
const collector = createTelemetryUsageCollector(usageCollection);
usageCollection.registerCollector(collector);
}

View file

@ -0,0 +1,61 @@
/*
* 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 { schema, TypeOf } from '@kbn/config-schema';
import { ENDPOINT_VERSION } from '../common/constants';
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
allowChangingOptInStatus: schema.boolean({ defaultValue: true }),
optIn: schema.conditional(
schema.siblingRef('allowChangingOptInStatus'),
schema.literal(false),
schema.maybe(schema.literal(true)),
schema.boolean({ defaultValue: true }),
{ defaultValue: true }
),
// `config` is used internally and not intended to be set
// config: Joi.string().default(getConfigPath()), TODO: Get it in some other way
banner: schema.boolean({ defaultValue: true }),
url: schema.conditional(
schema.contextRef('dev'),
schema.literal(true),
schema.string({
defaultValue: `https://telemetry-staging.elastic.co/xpack/${ENDPOINT_VERSION}/send`,
}),
schema.string({
defaultValue: `https://telemetry.elastic.co/xpack/${ENDPOINT_VERSION}/send`,
})
),
optInStatusUrl: schema.conditional(
schema.contextRef('dev'),
schema.literal(true),
schema.string({
defaultValue: `https://telemetry-staging.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`,
}),
schema.string({
defaultValue: `https://telemetry.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`,
})
),
sendUsageFrom: schema.oneOf([schema.literal('server'), schema.literal('browser')], {
defaultValue: 'browser',
}),
});
export type TelemetryConfigType = TypeOf<typeof configSchema>;

View file

@ -18,133 +18,75 @@
*/
import moment from 'moment';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
// @ts-ignore
import fetch from 'node-fetch';
import { telemetryCollectionManager } from './collection_manager';
import { TelemetryCollectionManagerPluginStart } from 'src/plugins/telemetry_collection_manager/server';
import {
PluginInitializerContext,
Logger,
SavedObjectsClientContract,
SavedObjectsClient,
CoreStart,
ICustomClusterClient,
} from '../../../core/server';
import {
getTelemetryOptIn,
getTelemetrySendUsageFrom,
getTelemetryFailureDetails,
} from './telemetry_config';
} from '../common/telemetry_config';
import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository';
import { REPORT_INTERVAL_MS } from '../common/constants';
import { TelemetryConfigType } from './config';
export interface FetcherTaskDepsStart {
telemetryCollectionManager: TelemetryCollectionManagerPluginStart;
}
export class FetcherTask {
private readonly initialCheckDelayMs = 60 * 1000 * 5;
private readonly checkIntervalMs = 60 * 1000 * 60 * 12;
private readonly config$: Observable<TelemetryConfigType>;
private readonly currentKibanaVersion: string;
private readonly logger: Logger;
private intervalId?: NodeJS.Timeout;
private lastReported?: number;
private currentVersion: string;
private isSending = false;
private server: any;
private internalRepository?: SavedObjectsClientContract;
private telemetryCollectionManager?: TelemetryCollectionManagerPluginStart;
private elasticsearchClient?: ICustomClusterClient;
constructor(server: any) {
this.server = server;
this.currentVersion = this.server.config().get('pkg.version');
constructor(initializerContext: PluginInitializerContext<TelemetryConfigType>) {
this.config$ = initializerContext.config.create();
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
this.logger = initializerContext.logger.get('fetcher');
}
private getInternalRepository = () => {
const { getSavedObjectsRepository } = this.server.savedObjects;
const { callWithInternalUser } = this.server.plugins.elasticsearch.getCluster('admin');
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
return internalRepository;
};
public start(
{ savedObjects, elasticsearch }: CoreStart,
{ telemetryCollectionManager }: FetcherTaskDepsStart
) {
this.internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository());
this.telemetryCollectionManager = telemetryCollectionManager;
this.elasticsearchClient = elasticsearch.legacy.createClient('telemetry-fetcher');
private getCurrentConfigs = async () => {
const internalRepository = this.getInternalRepository();
const telemetrySavedObject = await getTelemetrySavedObject(internalRepository);
const config = this.server.config();
const currentKibanaVersion = config.get('pkg.version');
const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom');
const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus');
const configTelemetryOptIn = config.get('telemetry.optIn');
const telemetryUrl = config.get('telemetry.url') as string;
const { failureCount, failureVersion } = await getTelemetryFailureDetails({
telemetrySavedObject,
});
setTimeout(() => {
this.sendIfDue();
this.intervalId = setInterval(() => this.sendIfDue(), this.checkIntervalMs);
}, this.initialCheckDelayMs);
}
return {
telemetryOptIn: getTelemetryOptIn({
currentKibanaVersion,
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn,
}),
telemetrySendUsageFrom: getTelemetrySendUsageFrom({
telemetrySavedObject,
configTelemetrySendUsageFrom,
}),
telemetryUrl,
failureCount,
failureVersion,
};
};
private updateLastReported = async () => {
const internalRepository = this.getInternalRepository();
this.lastReported = Date.now();
updateTelemetrySavedObject(internalRepository, {
reportFailureCount: 0,
lastReported: this.lastReported,
});
};
private updateReportFailure = async ({ failureCount }: { failureCount: number }) => {
const internalRepository = this.getInternalRepository();
updateTelemetrySavedObject(internalRepository, {
reportFailureCount: failureCount + 1,
reportFailureVersion: this.currentVersion,
});
};
private shouldSendReport = ({
telemetryOptIn,
telemetrySendUsageFrom,
reportFailureCount,
currentVersion,
reportFailureVersion,
}: any) => {
if (reportFailureCount > 2 && reportFailureVersion === currentVersion) {
return false;
public stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
if (telemetryOptIn && telemetrySendUsageFrom === 'server') {
if (!this.lastReported || Date.now() - this.lastReported > REPORT_INTERVAL_MS) {
return true;
}
if (this.elasticsearchClient) {
this.elasticsearchClient.close();
}
return false;
};
}
private fetchTelemetry = async () => {
return await telemetryCollectionManager.getStats({
unencrypted: false,
server: this.server,
start: moment()
.subtract(20, 'minutes')
.toISOString(),
end: moment().toISOString(),
});
};
private sendTelemetry = async (url: string, cluster: any): Promise<void> => {
this.server.log(['debug', 'telemetry', 'fetcher'], `Sending usage stats.`);
/**
* send OPTIONS before sending usage data.
* OPTIONS is less intrusive as it does not contain any payload and is used here to check if the endpoint is reachable.
*/
await fetch(url, {
method: 'options',
});
await fetch(url, {
method: 'post',
body: cluster,
});
};
private sendIfDue = async () => {
private async sendIfDue() {
if (this.isSending) {
return;
}
@ -165,24 +107,97 @@ export class FetcherTask {
} catch (err) {
await this.updateReportFailure(telemetryConfig);
this.server.log(
['warning', 'telemetry', 'fetcher'],
`Error sending telemetry usage data: ${err}`
);
this.logger.warn(`Error sending telemetry usage data: ${err}`);
}
this.isSending = false;
};
}
public start = () => {
setTimeout(() => {
this.sendIfDue();
this.intervalId = setInterval(() => this.sendIfDue(), this.checkIntervalMs);
}, this.initialCheckDelayMs);
};
private async getCurrentConfigs() {
const telemetrySavedObject = await getTelemetrySavedObject(this.internalRepository!);
const config = await this.config$.pipe(take(1)).toPromise();
const currentKibanaVersion = this.currentKibanaVersion;
const configTelemetrySendUsageFrom = config.sendUsageFrom;
const allowChangingOptInStatus = config.allowChangingOptInStatus;
const configTelemetryOptIn = typeof config.optIn === 'undefined' ? null : config.optIn;
const telemetryUrl = config.url;
const { failureCount, failureVersion } = getTelemetryFailureDetails({
telemetrySavedObject,
});
public stop = () => {
if (this.intervalId) {
clearInterval(this.intervalId);
return {
telemetryOptIn: getTelemetryOptIn({
currentKibanaVersion,
telemetrySavedObject,
allowChangingOptInStatus,
configTelemetryOptIn,
}),
telemetrySendUsageFrom: getTelemetrySendUsageFrom({
telemetrySavedObject,
configTelemetrySendUsageFrom,
}),
telemetryUrl,
failureCount,
failureVersion,
};
}
private async updateLastReported() {
this.lastReported = Date.now();
updateTelemetrySavedObject(this.internalRepository!, {
reportFailureCount: 0,
lastReported: this.lastReported,
});
}
private async updateReportFailure({ failureCount }: { failureCount: number }) {
updateTelemetrySavedObject(this.internalRepository!, {
reportFailureCount: failureCount + 1,
reportFailureVersion: this.currentKibanaVersion,
});
}
private shouldSendReport({
telemetryOptIn,
telemetrySendUsageFrom,
reportFailureCount,
currentVersion,
reportFailureVersion,
}: any) {
if (reportFailureCount > 2 && reportFailureVersion === currentVersion) {
return false;
}
};
if (telemetryOptIn && telemetrySendUsageFrom === 'server') {
if (!this.lastReported || Date.now() - this.lastReported > REPORT_INTERVAL_MS) {
return true;
}
}
return false;
}
private async fetchTelemetry() {
return await this.telemetryCollectionManager!.getStats({
unencrypted: false,
start: moment()
.subtract(20, 'minutes')
.toISOString(),
end: moment().toISOString(),
});
}
private async sendTelemetry(url: string, cluster: any): Promise<void> {
this.logger.debug(`Sending usage stats.`);
/**
* send OPTIONS before sending usage data.
* OPTIONS is less intrusive as it does not contain any payload and is used here to check if the endpoint is reachable.
*/
await fetch(url, {
method: 'options',
});
await fetch(url, {
method: 'post',
body: cluster,
});
}
}

View file

@ -26,27 +26,25 @@
* @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed.
*/
import { Server } from 'hapi';
import { IUiSettingsClient, SavedObjectsClientContract } from 'kibana/server';
import { CONFIG_TELEMETRY } from '../../common/constants';
import { updateTelemetrySavedObject } from '../telemetry_repository';
const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport';
export async function handleOldSettings(server: Server) {
const { getSavedObjectsRepository } = server.savedObjects;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const savedObjectsClient = getSavedObjectsRepository(callWithInternalUser);
const uiSettings = server.uiSettingsServiceFactory({ savedObjectsClient });
const oldTelemetrySetting = await uiSettings.get(CONFIG_TELEMETRY);
const oldAllowReportSetting = await uiSettings.get(CONFIG_ALLOW_REPORT);
export async function handleOldSettings(
savedObjectsClient: SavedObjectsClientContract,
uiSettingsClient: IUiSettingsClient
) {
const oldTelemetrySetting = await uiSettingsClient.get(CONFIG_TELEMETRY);
const oldAllowReportSetting = await uiSettingsClient.get(CONFIG_ALLOW_REPORT);
let legacyOptInValue = null;
if (typeof oldTelemetrySetting === 'boolean') {
legacyOptInValue = oldTelemetrySetting;
} else if (
typeof oldAllowReportSetting === 'boolean' &&
uiSettings.isOverridden(CONFIG_ALLOW_REPORT)
uiSettingsClient.isOverridden(CONFIG_ALLOW_REPORT)
) {
legacyOptInValue = oldAllowReportSetting;
}

View file

@ -17,15 +17,34 @@
* under the License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server';
import { TelemetryPlugin } from './plugin';
import * as constants from '../common/constants';
import { configSchema, TelemetryConfigType } from './config';
export { FetcherTask } from './fetcher';
export { replaceTelemetryInjectedVars } from './telemetry_config';
export { handleOldSettings } from './handle_old_settings';
export { telemetryCollectionManager } from './collection_manager';
export { PluginsSetup } from './plugin';
export const telemetryPlugin = (initializerContext: PluginInitializerContext) =>
export { TelemetryPluginsSetup } from './plugin';
export const config: PluginConfigDescriptor<TelemetryConfigType> = {
schema: configSchema,
exposeToBrowser: {
enabled: true,
url: true,
banner: true,
allowChangingOptInStatus: true,
optIn: true,
optInStatusUrl: true,
sendUsageFrom: true,
},
};
export const plugin = (initializerContext: PluginInitializerContext<TelemetryConfigType>) =>
new TelemetryPlugin(initializerContext);
export { constants };
export {
getClusterUuids,
getLocalLicense,
getLocalStats,
TelemetryLocalStats,
} from './telemetry_collection';

View file

@ -0,0 +1,168 @@
/*
* 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 { Observable } from 'rxjs';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
} from 'src/plugins/telemetry_collection_manager/server';
import {
CoreSetup,
PluginInitializerContext,
ISavedObjectsRepository,
CoreStart,
IUiSettingsClient,
SavedObjectsClient,
Plugin,
Logger,
} from '../../../core/server';
import { registerRoutes } from './routes';
import { registerCollection } from './telemetry_collection';
import {
registerUiMetricUsageCollector,
registerTelemetryUsageCollector,
registerTelemetryPluginUsageCollector,
registerManagementUsageCollector,
registerApplicationUsageCollector,
} from './collectors';
import { TelemetryConfigType } from './config';
import { FetcherTask } from './fetcher';
import { handleOldSettings } from './handle_old_settings';
export interface TelemetryPluginsSetup {
usageCollection: UsageCollectionSetup;
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup;
}
export interface TelemetryPluginsStart {
telemetryCollectionManager: TelemetryCollectionManagerPluginStart;
}
type SavedObjectsRegisterType = CoreSetup['savedObjects']['registerType'];
export class TelemetryPlugin implements Plugin {
private readonly logger: Logger;
private readonly currentKibanaVersion: string;
private readonly config$: Observable<TelemetryConfigType>;
private readonly isDev: boolean;
private readonly fetcherTask: FetcherTask;
private savedObjectsClient?: ISavedObjectsRepository;
private uiSettingsClient?: IUiSettingsClient;
constructor(initializerContext: PluginInitializerContext<TelemetryConfigType>) {
this.logger = initializerContext.logger.get();
this.isDev = initializerContext.env.mode.dev;
this.currentKibanaVersion = initializerContext.env.packageInfo.version;
this.config$ = initializerContext.config.create();
this.fetcherTask = new FetcherTask({
...initializerContext,
logger: this.logger,
});
}
public async setup(
core: CoreSetup,
{ usageCollection, telemetryCollectionManager }: TelemetryPluginsSetup
) {
const currentKibanaVersion = this.currentKibanaVersion;
const config$ = this.config$;
const isDev = this.isDev;
registerCollection(telemetryCollectionManager, core.elasticsearch.dataClient);
const router = core.http.createRouter();
registerRoutes({
config$,
currentKibanaVersion,
isDev,
router,
telemetryCollectionManager,
});
this.registerMappings(opts => core.savedObjects.registerType(opts));
this.registerUsageCollectors(usageCollection, opts => core.savedObjects.registerType(opts));
}
public async start(core: CoreStart, { telemetryCollectionManager }: TelemetryPluginsStart) {
const { savedObjects, uiSettings } = core;
this.savedObjectsClient = savedObjects.createInternalRepository();
const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient);
this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
try {
await handleOldSettings(savedObjectsClient, this.uiSettingsClient);
} catch (error) {
this.logger.warn('Unable to update legacy telemetry configs.');
}
this.fetcherTask.start(core, { telemetryCollectionManager });
}
private registerMappings(registerType: SavedObjectsRegisterType) {
registerType({
name: 'telemetry',
hidden: false,
namespaceAgnostic: true,
mappings: {
properties: {
enabled: {
type: 'boolean',
},
sendUsageFrom: {
type: 'keyword',
},
lastReported: {
type: 'date',
},
lastVersionChecked: {
type: 'keyword',
},
userHasSeenNotice: {
type: 'boolean',
},
reportFailureCount: {
type: 'integer',
},
reportFailureVersion: {
type: 'keyword',
},
},
},
});
}
private registerUsageCollectors(
usageCollection: UsageCollectionSetup,
registerType: SavedObjectsRegisterType
) {
const getSavedObjectsClient = () => this.savedObjectsClient;
const getUiSettingsClient = () => this.uiSettingsClient;
registerTelemetryPluginUsageCollector(usageCollection, {
currentKibanaVersion: this.currentKibanaVersion,
config$: this.config$,
getSavedObjectsClient,
});
registerTelemetryUsageCollector(usageCollection);
registerManagementUsageCollector(usageCollection, getUiSettingsClient);
registerUiMetricUsageCollector(usageCollection, registerType, getSavedObjectsClient);
registerApplicationUsageCollector(usageCollection, registerType, getSavedObjectsClient);
}
}

View file

@ -17,22 +17,27 @@
* under the License.
*/
import { Legacy } from 'kibana';
import { CoreSetup } from 'src/core/server';
import { Observable } from 'rxjs';
import { IRouter } from 'kibana/server';
import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server';
import { registerTelemetryOptInRoutes } from './telemetry_opt_in';
import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats';
import { registerTelemetryOptInStatsRoutes } from './telemetry_opt_in_stats';
import { registerTelemetryUserHasSeenNotice } from './telemetry_user_has_seen_notice';
import { TelemetryConfigType } from '../config';
interface RegisterRoutesParams {
core: CoreSetup;
isDev: boolean;
config$: Observable<TelemetryConfigType>;
currentKibanaVersion: string;
server: Legacy.Server;
router: IRouter;
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup;
}
export function registerRoutes({ core, currentKibanaVersion, server }: RegisterRoutesParams) {
registerTelemetryOptInRoutes({ core, currentKibanaVersion, server });
registerTelemetryUsageStatsRoutes(server);
registerTelemetryOptInStatsRoutes(server);
registerTelemetryUserHasSeenNotice(server);
export function registerRoutes(options: RegisterRoutesParams) {
const { isDev, telemetryCollectionManager, router } = options;
registerTelemetryOptInRoutes(options);
registerTelemetryUsageStatsRoutes(router, telemetryCollectionManager, isDev);
registerTelemetryOptInStatsRoutes(router, telemetryCollectionManager);
registerTelemetryUserHasSeenNotice(router);
}

View file

@ -0,0 +1,101 @@
/*
* 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 moment from 'moment';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { schema } from '@kbn/config-schema';
import { IRouter } from 'kibana/server';
import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server';
import { getTelemetryAllowChangingOptInStatus } from '../../common/telemetry_config';
import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats';
import {
TelemetrySavedObjectAttributes,
updateTelemetrySavedObject,
getTelemetrySavedObject,
} from '../telemetry_repository';
import { TelemetryConfigType } from '../config';
interface RegisterOptInRoutesParams {
currentKibanaVersion: string;
router: IRouter;
config$: Observable<TelemetryConfigType>;
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup;
}
export function registerTelemetryOptInRoutes({
config$,
router,
currentKibanaVersion,
telemetryCollectionManager,
}: RegisterOptInRoutesParams) {
router.post(
{
path: '/api/telemetry/v2/optIn',
validate: {
body: schema.object({ enabled: schema.boolean() }),
},
},
async (context, req, res) => {
const newOptInStatus = req.body.enabled;
const attributes: TelemetrySavedObjectAttributes = {
enabled: newOptInStatus,
lastVersionChecked: currentKibanaVersion,
};
const config = await config$.pipe(take(1)).toPromise();
const telemetrySavedObject = await getTelemetrySavedObject(context.core.savedObjects.client);
if (telemetrySavedObject === false) {
// If we get false, we couldn't get the saved object due to lack of permissions
// so we can assume the user won't be able to update it either
return res.forbidden();
}
const configTelemetryAllowChangingOptInStatus = config.allowChangingOptInStatus;
const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({
telemetrySavedObject,
configTelemetryAllowChangingOptInStatus,
});
if (!allowChangingOptInStatus) {
return res.badRequest({
body: JSON.stringify({ error: 'Not allowed to change Opt-in Status.' }),
});
}
if (config.sendUsageFrom === 'server') {
const optInStatusUrl = config.optInStatusUrl;
await sendTelemetryOptInStatus(
telemetryCollectionManager,
{ optInStatusUrl, newOptInStatus },
{
start: moment()
.subtract(20, 'minutes')
.toISOString(),
end: moment().toISOString(),
unencrypted: false,
}
);
}
await updateTelemetrySavedObject(context.core.savedObjects.client, attributes);
return res.ok({});
}
);
}

View file

@ -19,10 +19,14 @@
// @ts-ignore
import fetch from 'node-fetch';
import Joi from 'joi';
import moment from 'moment';
import { Legacy } from 'kibana';
import { telemetryCollectionManager, StatsGetterConfig } from '../collection_manager';
import { IRouter } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import {
TelemetryCollectionManagerPluginSetup,
StatsGetterConfig,
} from 'src/plugins/telemetry_collection_manager/server';
interface SendTelemetryOptInStatusConfig {
optInStatusUrl: string;
@ -30,6 +34,7 @@ interface SendTelemetryOptInStatusConfig {
}
export async function sendTelemetryOptInStatus(
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup,
config: SendTelemetryOptInStatusConfig,
statsGetterConfig: StatsGetterConfig
) {
@ -45,41 +50,42 @@ export async function sendTelemetryOptInStatus(
});
}
export function registerTelemetryOptInStatsRoutes(server: Legacy.Server) {
server.route({
method: 'POST',
path: '/api/telemetry/v2/clusters/_opt_in_stats',
options: {
export function registerTelemetryOptInStatsRoutes(
router: IRouter,
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup
) {
router.post(
{
path: '/api/telemetry/v2/clusters/_opt_in_stats',
validate: {
payload: Joi.object({
enabled: Joi.bool().required(),
unencrypted: Joi.bool().default(true),
body: schema.object({
enabled: schema.boolean(),
unencrypted: schema.boolean({ defaultValue: true }),
}),
},
},
handler: async (req: any, h: any) => {
async (context, req, res) => {
try {
const newOptInStatus = req.payload.enabled;
const unencrypted = req.payload.unencrypted;
const statsGetterConfig = {
const newOptInStatus = req.body.enabled;
const unencrypted = req.body.unencrypted;
const statsGetterConfig: StatsGetterConfig = {
start: moment()
.subtract(20, 'minutes')
.toISOString(),
end: moment().toISOString(),
server: req.server,
req,
unencrypted,
request: req,
};
const optInStatus = await telemetryCollectionManager.getOptInStats(
newOptInStatus,
statsGetterConfig
);
return h.response(optInStatus).code(200);
return res.ok({ body: optInStatus });
} catch (err) {
return h.response([]).code(200);
return res.ok({ body: [] });
}
},
});
}
);
}

View file

@ -0,0 +1,82 @@
/*
* 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 moment from 'moment';
import { schema } from '@kbn/config-schema';
import { TypeOptions } from '@kbn/config-schema/target/types/types';
import { IRouter } from 'kibana/server';
import {
TelemetryCollectionManagerPluginSetup,
StatsGetterConfig,
} from 'src/plugins/telemetry_collection_manager/server';
const validate: TypeOptions<string | number>['validate'] = value => {
if (!moment(value).isValid()) {
return `${value} is not a valid date`;
}
};
const dateSchema = schema.oneOf([schema.string({ validate }), schema.number({ validate })]);
export function registerTelemetryUsageStatsRoutes(
router: IRouter,
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup,
isDev: boolean
) {
router.post(
{
path: '/api/telemetry/v2/clusters/_stats',
validate: {
body: schema.object({
unencrypted: schema.boolean({ defaultValue: false }),
timeRange: schema.object({
min: dateSchema,
max: dateSchema,
}),
}),
},
},
async (context, req, res) => {
const start = moment(req.body.timeRange.min).toISOString();
const end = moment(req.body.timeRange.max).toISOString();
const unencrypted = req.body.unencrypted;
try {
const statsConfig: StatsGetterConfig = {
unencrypted,
start,
end,
request: req,
};
const stats = await telemetryCollectionManager.getStats(statsConfig);
return res.ok({ body: stats });
} catch (err) {
if (isDev) {
// don't ignore errors when running in dev mode
throw err;
}
if (unencrypted && err.status === 403) {
return res.forbidden();
}
// ignore errors and return empty set
return res.ok({ body: [] });
}
}
);
}

View file

@ -17,8 +17,7 @@
* under the License.
*/
import { Legacy } from 'kibana';
import { Request } from 'hapi';
import { IRouter } from 'kibana/server';
import {
TelemetrySavedObject,
TelemetrySavedObjectAttributes,
@ -26,19 +25,14 @@ import {
updateTelemetrySavedObject,
} from '../telemetry_repository';
const getInternalRepository = (server: Legacy.Server) => {
const { getSavedObjectsRepository } = server.savedObjects;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
return internalRepository;
};
export function registerTelemetryUserHasSeenNotice(server: Legacy.Server) {
server.route({
method: 'PUT',
path: '/api/telemetry/v2/userHasSeenNotice',
handler: async (req: Request): Promise<TelemetrySavedObjectAttributes> => {
const internalRepository = getInternalRepository(server);
export function registerTelemetryUserHasSeenNotice(router: IRouter) {
router.put(
{
path: '/api/telemetry/v2/userHasSeenNotice',
validate: false,
},
async (context, req, res) => {
const internalRepository = context.core.savedObjects.client;
const telemetrySavedObject: TelemetrySavedObject = await getTelemetrySavedObject(
internalRepository
);
@ -50,7 +44,7 @@ export function registerTelemetryUserHasSeenNotice(server: Legacy.Server) {
};
await updateTelemetrySavedObject(internalRepository, updatedAttributes);
return updatedAttributes;
},
});
return res.ok({ body: updatedAttributes });
}
);
}

View file

@ -128,9 +128,14 @@ describe('get_local_stats', () => {
},
};
const context = {
logger: console,
version: '8.0.0',
};
describe('handleLocalStats', () => {
it('returns expected object without xpack and kibana data', () => {
const result = handleLocalStats(getMockServer(), clusterInfo, clusterStats);
const result = handleLocalStats(clusterInfo, clusterStats, void 0, context);
expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid);
expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name);
expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats);
@ -141,7 +146,7 @@ describe('get_local_stats', () => {
});
it('returns expected object with xpack', () => {
const result = handleLocalStats(getMockServer(), clusterInfo, clusterStats);
const result = handleLocalStats(clusterInfo, clusterStats, void 0, context);
const { stack_stats: stack, ...cluster } = result;
expect(cluster.collection).to.be(combinedStatsResult.collection);
expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid);

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { APICaller } from 'kibana/server';
// This can be removed when the ES client improves the types
export interface ESClusterInfo {
@ -43,6 +43,6 @@ export interface ESClusterInfo {
*
* @param {function} callCluster The callWithInternalUser handler (exposed for testing)
*/
export function getClusterInfo(callCluster: CallCluster) {
export function getClusterInfo(callCluster: APICaller) {
return callCluster<ESClusterInfo>('info');
}

View file

@ -17,15 +17,15 @@
* under the License.
*/
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { ClusterDetailsGetter } from 'src/plugins/telemetry_collection_manager/server';
import { APICaller } from 'kibana/server';
import { TIMEOUT } from './constants';
import { ClusterDetailsGetter } from '../collection_manager';
/**
* Get the cluster stats from the connected cluster.
*
* This is the equivalent to GET /_cluster/stats?timeout=30s.
*/
export async function getClusterStats(callCluster: CallCluster) {
export async function getClusterStats(callCluster: APICaller) {
return await callCluster('cluster.stats', {
timeout: TIMEOUT,
});

View file

@ -19,7 +19,8 @@
import { omit } from 'lodash';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { APICaller } from 'kibana/server';
import { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server';
export interface KibanaUsageStats {
kibana: {
@ -37,12 +38,12 @@ export interface KibanaUsageStats {
[plugin: string]: any;
}
export function handleKibanaStats(server: any, response?: KibanaUsageStats) {
export function handleKibanaStats(
{ logger, version: serverVersion }: StatsCollectionContext,
response?: KibanaUsageStats
) {
if (!response) {
server.log(
['warning', 'telemetry', 'local-stats'],
'No Kibana stats returned from usage collectors'
);
logger.warn('No Kibana stats returned from usage collectors');
return;
}
@ -60,10 +61,7 @@ export function handleKibanaStats(server: any, response?: KibanaUsageStats) {
};
}, {});
const version = server
.config()
.get('pkg.version')
.replace(/-snapshot/i, '');
const version = serverVersion.replace(/-snapshot/i, ''); // Shouldn't we better maintain the -snapshot so we can differentiate between actual final releases and snapshots?
// combine core stats (os types, saved objects) with plugin usage stats
// organize the object into the same format as monitoring-enabled telemetry
@ -79,7 +77,7 @@ export function handleKibanaStats(server: any, response?: KibanaUsageStats) {
export async function getKibana(
usageCollection: UsageCollectionSetup,
callWithInternalUser: CallCluster
callWithInternalUser: APICaller
): Promise<KibanaUsageStats> {
const usage = await usageCollection.bulkFetch(callWithInternalUser);
return usageCollection.toObject(usage);

View file

@ -17,26 +17,12 @@
* under the License.
*/
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { LicenseGetter } from '../collection_manager';
import { APICaller } from 'kibana/server';
import { ESLicense, LicenseGetter } from 'src/plugins/telemetry_collection_manager/server';
// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html
export interface ESLicense {
status: string;
uid: string;
type: string;
issue_date: string;
issue_date_in_millis: number;
expiry_date: string;
expirty_date_in_millis: number;
max_nodes: number;
issued_to: string;
issuer: string;
start_date_in_millis: number;
}
let cachedLicense: ESLicense | undefined;
function fetchLicense(callCluster: CallCluster, local: boolean) {
function fetchLicense(callCluster: APICaller, local: boolean) {
return callCluster<{ license: ESLicense }>('transport.request', {
method: 'GET',
path: '/_license',
@ -55,7 +41,7 @@ function fetchLicense(callCluster: CallCluster, local: boolean) {
*
* Like any X-Pack related API, X-Pack must installed for this to work.
*/
async function getLicenseFromLocalOrMaster(callCluster: CallCluster) {
async function getLicenseFromLocalOrMaster(callCluster: APICaller) {
// Fetching the local license is cheaper than getting it from the master and good enough
const { license } = await fetchLicense(callCluster, true).catch(async err => {
if (cachedLicense) {

View file

@ -17,10 +17,13 @@
* under the License.
*/
import {
StatsGetter,
StatsCollectionContext,
} from 'src/plugins/telemetry_collection_manager/server';
import { getClusterInfo, ESClusterInfo } from './get_cluster_info';
import { getClusterStats } from './get_cluster_stats';
import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana';
import { StatsGetter } from '../collection_manager';
/**
* Handle the separate local calls by combining them into a single object response that looks like the
@ -32,10 +35,10 @@ import { StatsGetter } from '../collection_manager';
* @param {Object} kibana The Kibana Usage stats
*/
export function handleLocalStats(
server: any,
{ cluster_name, cluster_uuid, version }: ESClusterInfo,
{ _nodes, cluster_name: clusterName, ...clusterStats }: any,
kibana: KibanaUsageStats
kibana: KibanaUsageStats,
context: StatsCollectionContext
) {
return {
timestamp: new Date().toISOString(),
@ -45,7 +48,7 @@ export function handleLocalStats(
cluster_stats: clusterStats,
collection: 'local',
stack_stats: {
kibana: handleKibanaStats(server, kibana),
kibana: handleKibanaStats(context, kibana),
},
};
}
@ -55,8 +58,12 @@ export type TelemetryLocalStats = ReturnType<typeof handleLocalStats>;
/**
* Get statistics for all products joined by Elasticsearch cluster.
*/
export const getLocalStats: StatsGetter<TelemetryLocalStats> = async (clustersDetails, config) => {
const { server, callCluster, usageCollection } = config;
export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async (
clustersDetails,
config,
context
) => {
const { callCluster, usageCollection } = config;
return await Promise.all(
clustersDetails.map(async clustersDetail => {
@ -65,7 +72,7 @@ export const getLocalStats: StatsGetter<TelemetryLocalStats> = async (clustersDe
getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_)
getKibana(usageCollection, callCluster),
]);
return handleLocalStats(server, clusterInfo, clusterStats, kibana);
return handleLocalStats(clusterInfo, clusterStats, kibana, context);
})
);
};

View file

@ -17,6 +17,7 @@
* under the License.
*/
export { getLocalStats } from './get_local_stats';
export { getLocalStats, TelemetryLocalStats } from './get_local_stats';
export { getLocalLicense } from './get_local_license';
export { getClusterUuids } from './get_cluster_stats';
export { registerCollection } from './register_collection';

View file

@ -36,14 +36,18 @@
* under the License.
*/
import { telemetryCollectionManager } from '../collection_manager';
import { IClusterClient } from 'kibana/server';
import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server';
import { getLocalStats } from './get_local_stats';
import { getClusterUuids } from './get_cluster_stats';
import { getLocalLicense } from './get_local_license';
export function registerCollection() {
export function registerCollection(
telemetryCollectionManager: TelemetryCollectionManagerPluginSetup,
esCluster: IClusterClient
) {
telemetryCollectionManager.setCollection({
esCluster: 'data',
esCluster,
title: 'local',
priority: 0,
statsGetter: getLocalStats,

View file

@ -18,7 +18,7 @@
*/
import { getTelemetrySavedObject } from './get_telemetry_saved_object';
import { SavedObjectsErrorHelpers } from '../../../../../core/server';
import { SavedObjectsErrorHelpers } from '../../../../core/server';
describe('getTelemetrySavedObject', () => {
it('returns null when saved object not found', async () => {
@ -79,7 +79,7 @@ function getCallGetTelemetrySavedObjectParams(
async function callGetTelemetrySavedObject(params: CallGetTelemetrySavedObjectParams) {
const savedObjectsClient = getMockSavedObjectsClient(params);
return await getTelemetrySavedObject(savedObjectsClient);
return await getTelemetrySavedObject(savedObjectsClient as any);
}
const SavedObjectForbiddenMessage = 'savedObjectForbidden';

View file

@ -17,13 +17,16 @@
* under the License.
*/
import { TelemetrySavedObjectAttributes } from './';
import { SavedObjectsErrorHelpers } from '../../../../../core/server';
import { SavedObjectsErrorHelpers, SavedObjectsClientContract } from '../../../../core/server';
import { TelemetrySavedObject } from './';
export type TelemetrySavedObject = TelemetrySavedObjectAttributes | null | false;
type GetTelemetrySavedObject = (repository: any) => Promise<TelemetrySavedObject>;
type GetTelemetrySavedObject = (
repository: SavedObjectsClientContract
) => Promise<TelemetrySavedObject>;
export const getTelemetrySavedObject: GetTelemetrySavedObject = async (repository: any) => {
export const getTelemetrySavedObject: GetTelemetrySavedObject = async (
repository: SavedObjectsClientContract
) => {
try {
const { attributes } = await repository.get('telemetry', 'telemetry');
return attributes;

View file

@ -0,0 +1,25 @@
/*
* 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.
*/
export { getTelemetrySavedObject } from './get_telemetry_saved_object';
export { updateTelemetrySavedObject } from './update_telemetry_saved_object';
export {
TelemetrySavedObject,
TelemetrySavedObjectAttributes,
} from '../../common/telemetry_config/types';

View file

@ -17,11 +17,11 @@
* under the License.
*/
import { SavedObjectsErrorHelpers, SavedObjectsClientContract } from '../../../../core/server';
import { TelemetrySavedObjectAttributes } from './';
import { SavedObjectsErrorHelpers } from '../../../../../core/server';
export async function updateTelemetrySavedObject(
savedObjectsClient: any,
savedObjectsClient: SavedObjectsClientContract,
savedObjectAttributes: TelemetrySavedObjectAttributes
) {
try {

View file

@ -0,0 +1,7 @@
# Telemetry Collection Manager
Telemetry's collection manager to go through all the telemetry sources when fetching it before reporting.
It has been split into a separate plugin because the `telemetry` plugin was pretty much being a passthrough in many cases to instantiate and maintain the logic of this bit.
For separation of concerns, it's better to have this piece of logic independent to the rest.

View file

@ -17,4 +17,5 @@
* under the License.
*/
import './management';
export const PLUGIN_ID = 'telemetryCollectionManager';
export const PLUGIN_NAME = 'telemetry_collection_manager';

View file

@ -0,0 +1,10 @@
{
"id": "telemetryCollectionManager",
"version": "kibana",
"server": true,
"ui": false,
"requiredPlugins": [
"usageCollection"
],
"optionalPlugins": []
}

View file

@ -0,0 +1,41 @@
/*
* 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 { PluginInitializerContext } from 'kibana/server';
import { TelemetryCollectionManagerPlugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin(initializerContext: PluginInitializerContext) {
return new TelemetryCollectionManagerPlugin(initializerContext);
}
export {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
ESLicense,
StatsCollectionConfig,
StatsGetter,
StatsGetterConfig,
StatsCollectionContext,
ClusterDetails,
ClusterDetailsGetter,
LicenseGetter,
} from './types';

View file

@ -0,0 +1,253 @@
/*
* 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import {
PluginInitializerContext,
CoreSetup,
CoreStart,
Plugin,
Logger,
} from '../../../core/server';
import {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
BasicStatsPayload,
CollectionConfig,
Collection,
StatsGetterConfig,
StatsCollectionConfig,
UsageStatsPayload,
StatsCollectionContext,
} from './types';
import { encryptTelemetry } from './encryption';
interface TelemetryCollectionPluginsDepsSetup {
usageCollection: UsageCollectionSetup;
}
export class TelemetryCollectionManagerPlugin
implements Plugin<TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart> {
private readonly logger: Logger;
private readonly collections: Array<Collection<any>> = [];
private usageGetterMethodPriority = -1;
private usageCollection?: UsageCollectionSetup;
private readonly isDev: boolean;
private readonly version: string;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
this.isDev = initializerContext.env.mode.dev;
this.version = initializerContext.env.packageInfo.version;
}
public setup(core: CoreSetup, { usageCollection }: TelemetryCollectionPluginsDepsSetup) {
this.usageCollection = usageCollection;
return {
setCollection: this.setCollection.bind(this),
getOptInStats: this.getOptInStats.bind(this),
getStats: this.getStats.bind(this),
};
}
public start(core: CoreStart) {
return {
setCollection: this.setCollection.bind(this),
getOptInStats: this.getOptInStats.bind(this),
getStats: this.getStats.bind(this),
};
}
public stop() {}
private setCollection<CustomContext extends Record<string, any>, T extends BasicStatsPayload>(
collectionConfig: CollectionConfig<CustomContext, T>
) {
const {
title,
priority,
esCluster,
statsGetter,
clusterDetailsGetter,
licenseGetter,
} = collectionConfig;
if (typeof priority !== 'number') {
throw new Error('priority must be set.');
}
if (priority === this.usageGetterMethodPriority) {
throw new Error(`A Usage Getter with the same priority is already set.`);
}
if (priority > this.usageGetterMethodPriority) {
if (!statsGetter) {
throw Error('Stats getter method not set.');
}
if (!esCluster) {
throw Error('esCluster name must be set for the getCluster method.');
}
if (!clusterDetailsGetter) {
throw Error('Cluster UUIds method is not set.');
}
if (!licenseGetter) {
throw Error('License getter method not set.');
}
this.collections.unshift({
licenseGetter,
statsGetter,
clusterDetailsGetter,
esCluster,
title,
});
this.usageGetterMethodPriority = priority;
}
}
private getStatsCollectionConfig(
config: StatsGetterConfig,
collection: Collection,
usageCollection: UsageCollectionSetup
): StatsCollectionConfig {
const { start, end, request } = config;
const callCluster = config.unencrypted
? collection.esCluster.asScoped(request).callAsCurrentUser
: collection.esCluster.callAsInternalUser;
return { callCluster, start, end, usageCollection };
}
private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) {
if (!this.usageCollection) {
return [];
}
for (const collection of this.collections) {
const statsCollectionConfig = this.getStatsCollectionConfig(
config,
collection,
this.usageCollection
);
try {
const optInStats = await this.getOptInStatsForCollection(
collection,
optInStatus,
statsCollectionConfig
);
if (optInStats && optInStats.length) {
this.logger.debug(`Got Opt In stats using ${collection.title} collection.`);
if (config.unencrypted) {
return optInStats;
}
return encryptTelemetry(optInStats, this.isDev);
}
} catch (err) {
this.logger.debug(`Failed to collect any opt in stats with registered collections.`);
// swallow error to try next collection;
}
}
return [];
}
private getOptInStatsForCollection = async (
collection: Collection,
optInStatus: boolean,
statsCollectionConfig: StatsCollectionConfig
) => {
const context: StatsCollectionContext = {
logger: this.logger.get(collection.title),
isDev: this.isDev,
version: this.version,
...collection.customContext,
};
const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig, context);
return clustersDetails.map(({ clusterUuid }) => ({
cluster_uuid: clusterUuid,
opt_in_status: optInStatus,
}));
};
private async getStats(config: StatsGetterConfig) {
if (!this.usageCollection) {
return [];
}
for (const collection of this.collections) {
const statsCollectionConfig = this.getStatsCollectionConfig(
config,
collection,
this.usageCollection
);
try {
const usageData = await this.getUsageForCollection(collection, statsCollectionConfig);
if (usageData.length) {
this.logger.debug(`Got Usage using ${collection.title} collection.`);
if (config.unencrypted) {
return usageData;
}
return encryptTelemetry(usageData, this.isDev);
}
} catch (err) {
this.logger.debug(
`Failed to collect any usage with registered collection ${collection.title}.`
);
// swallow error to try next collection;
}
}
return [];
}
private async getUsageForCollection(
collection: Collection,
statsCollectionConfig: StatsCollectionConfig
): Promise<UsageStatsPayload[]> {
const context: StatsCollectionContext = {
logger: this.logger.get(collection.title),
isDev: this.isDev,
version: this.version,
...collection.customContext,
};
const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig, context);
if (clustersDetails.length === 0) {
// don't bother doing a further lookup, try next collection.
return [];
}
const [stats, licenses] = await Promise.all([
collection.statsGetter(clustersDetails, statsCollectionConfig, context),
collection.licenseGetter(clustersDetails, statsCollectionConfig, context),
]);
return stats.map(stat => {
const license = licenses[stat.cluster_uuid];
return {
...(license ? { license } : {}),
...stat,
collectionSource: collection.title,
};
});
}
}

View file

@ -0,0 +1,150 @@
/*
* 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 { APICaller, Logger, KibanaRequest, IClusterClient } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { TelemetryCollectionManagerPlugin } from './plugin';
export interface TelemetryCollectionManagerPluginSetup {
setCollection: <CustomContext extends Record<string, any>, T extends BasicStatsPayload>(
collectionConfig: CollectionConfig<CustomContext, T>
) => void;
getOptInStats: TelemetryCollectionManagerPlugin['getOptInStats'];
getStats: TelemetryCollectionManagerPlugin['getStats'];
}
export interface TelemetryCollectionManagerPluginStart {
setCollection: <CustomContext extends Record<string, any>, T extends BasicStatsPayload>(
collectionConfig: CollectionConfig<CustomContext, T>
) => void;
getOptInStats: TelemetryCollectionManagerPlugin['getOptInStats'];
getStats: TelemetryCollectionManagerPlugin['getStats'];
}
export interface TelemetryOptInStats {
cluster_uuid: string;
opt_in_status: boolean;
}
export interface BaseStatsGetterConfig {
unencrypted: boolean;
start: string;
end: string;
request?: KibanaRequest;
}
export interface EncryptedStatsGetterConfig extends BaseStatsGetterConfig {
unencrypted: false;
}
export interface UnencryptedStatsGetterConfig extends BaseStatsGetterConfig {
unencrypted: true;
request: KibanaRequest;
}
export interface ClusterDetails {
clusterUuid: string;
}
export interface StatsCollectionConfig {
usageCollection: UsageCollectionSetup;
callCluster: APICaller;
start: string | number;
end: string | number;
}
export interface BasicStatsPayload {
timestamp: string;
cluster_uuid: string;
cluster_name: string;
version: string;
cluster_stats: object;
collection?: string;
stack_stats: object;
}
export interface UsageStatsPayload extends BasicStatsPayload {
license?: ESLicense;
collectionSource: string;
}
// From https://www.elastic.co/guide/en/elasticsearch/reference/current/get-license.html
export interface ESLicense {
status: string;
uid: string;
type: string;
issue_date: string;
issue_date_in_millis: number;
expiry_date: string;
expirty_date_in_millis: number;
max_nodes: number;
issued_to: string;
issuer: string;
start_date_in_millis: number;
}
export interface StatsCollectionContext {
logger: Logger;
isDev: boolean;
version: string;
}
export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig;
export type ClusterDetailsGetter<CustomContext extends Record<string, any> = {}> = (
config: StatsCollectionConfig,
context: StatsCollectionContext & CustomContext
) => Promise<ClusterDetails[]>;
export type StatsGetter<
CustomContext extends Record<string, any> = {},
T extends BasicStatsPayload = BasicStatsPayload
> = (
clustersDetails: ClusterDetails[],
config: StatsCollectionConfig,
context: StatsCollectionContext & CustomContext
) => Promise<T[]>;
export type LicenseGetter<CustomContext extends Record<string, any> = {}> = (
clustersDetails: ClusterDetails[],
config: StatsCollectionConfig,
context: StatsCollectionContext & CustomContext
) => Promise<{ [clusterUuid: string]: ESLicense | undefined }>;
export interface CollectionConfig<
CustomContext extends Record<string, any> = {},
T extends BasicStatsPayload = BasicStatsPayload
> {
title: string;
priority: number;
esCluster: IClusterClient;
statsGetter: StatsGetter<CustomContext, T>;
clusterDetailsGetter: ClusterDetailsGetter<CustomContext>;
licenseGetter: LicenseGetter<CustomContext>;
customContext?: CustomContext;
}
export interface Collection<
CustomContext extends Record<string, any> = {},
T extends BasicStatsPayload = BasicStatsPayload
> {
customContext?: CustomContext;
statsGetter: StatsGetter<CustomContext, T>;
licenseGetter: LicenseGetter<CustomContext>;
clusterDetailsGetter: ClusterDetailsGetter<CustomContext>;
esCluster: IClusterClient;
title: string;
}

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