mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
d8d06e7343
commit
452193fdba
149 changed files with 2161 additions and 1654 deletions
|
@ -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"],
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "application_usage",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "telemetry",
|
||||
"version": "kibana"
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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();
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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() {},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"ui-metric": {
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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);
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 () => {
|
|
@ -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);
|
2
src/legacy/server/kbn_server.d.ts
vendored
2
src/legacy/server/kbn_server.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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)
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object';
|
||||
import { TelemetrySavedObject } from './types';
|
||||
|
||||
interface NotifyOpts {
|
||||
allowChangingOptInStatus: boolean;
|
|
@ -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', () => {
|
|
@ -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;
|
|
@ -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', () => {
|
|
@ -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';
|
|
@ -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';
|
|
@ -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;
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"id": "telemetry",
|
||||
"version": "kibana",
|
||||
"server": false,
|
||||
"ui": true
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": [
|
||||
"telemetryCollectionManager",
|
||||
"usageCollection"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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', () => {
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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',
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks';
|
||||
import { savedObjectsRepositoryMock } from '../../../../core/server/mocks';
|
||||
|
||||
import { findAll } from './find_all';
|
||||
|
|
@ -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';
|
|
@ -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);
|
|
@ -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);
|
|
@ -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();
|
|
@ -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 () => {
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
61
src/plugins/telemetry/server/config.ts
Normal file
61
src/plugins/telemetry/server/config.ts
Normal 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>;
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
168
src/plugins/telemetry/server/plugin.ts
Normal file
168
src/plugins/telemetry/server/plugin.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
101
src/plugins/telemetry/server/routes/telemetry_opt_in.ts
Normal file
101
src/plugins/telemetry/server/routes/telemetry_opt_in.ts
Normal 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({});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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: [] });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
82
src/plugins/telemetry/server/routes/telemetry_usage_stats.ts
Normal file
82
src/plugins/telemetry/server/routes/telemetry_usage_stats.ts
Normal 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: [] });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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);
|
|
@ -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');
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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);
|
|
@ -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) {
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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,
|
|
@ -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';
|
|
@ -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;
|
25
src/plugins/telemetry/server/telemetry_repository/index.ts
Normal file
25
src/plugins/telemetry/server/telemetry_repository/index.ts
Normal 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';
|
|
@ -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 {
|
7
src/plugins/telemetry_collection_manager/README.md
Normal file
7
src/plugins/telemetry_collection_manager/README.md
Normal 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.
|
|
@ -17,4 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './management';
|
||||
export const PLUGIN_ID = 'telemetryCollectionManager';
|
||||
export const PLUGIN_NAME = 'telemetry_collection_manager';
|
10
src/plugins/telemetry_collection_manager/kibana.json
Normal file
10
src/plugins/telemetry_collection_manager/kibana.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "telemetryCollectionManager",
|
||||
"version": "kibana",
|
||||
"server": true,
|
||||
"ui": false,
|
||||
"requiredPlugins": [
|
||||
"usageCollection"
|
||||
],
|
||||
"optionalPlugins": []
|
||||
}
|
41
src/plugins/telemetry_collection_manager/server/index.ts
Normal file
41
src/plugins/telemetry_collection_manager/server/index.ts
Normal 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';
|
253
src/plugins/telemetry_collection_manager/server/plugin.ts
Normal file
253
src/plugins/telemetry_collection_manager/server/plugin.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
150
src/plugins/telemetry_collection_manager/server/types.ts
Normal file
150
src/plugins/telemetry_collection_manager/server/types.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue