diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts
index fd0f612602f77..c42bb6a160a1f 100644
--- a/x-pack/plugins/monitoring/common/constants.ts
+++ b/x-pack/plugins/monitoring/common/constants.ts
@@ -276,3 +276,10 @@ export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365',
export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress';
export const XPACK_DEFAULT_ADMIN_EMAIL_UI_SETTING = 'xPack:defaultAdminEmail';
+
+/**
+ * The saved object type for various monitoring data
+ */
+export const SAVED_OBJECT_TELEMETRY = 'monitoring-telemetry';
+
+export const TELEMETRY_METRIC_BUTTON_CLICK = 'btnclick__';
diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json
index 2b8756ea0cb46..8b0b0b7aae693 100644
--- a/x-pack/plugins/monitoring/kibana.json
+++ b/x-pack/plugins/monitoring/kibana.json
@@ -12,7 +12,8 @@
"triggers_actions_ui",
"alerts",
"actions",
- "encryptedSavedObjects"
+ "encryptedSavedObjects",
+ "observability"
],
"optionalPlugins": ["infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"],
"server": true,
diff --git a/x-pack/plugins/monitoring/public/angular/index.ts b/x-pack/plugins/monitoring/public/angular/index.ts
index da57c028643a5..3c30d3c358a14 100644
--- a/x-pack/plugins/monitoring/public/angular/index.ts
+++ b/x-pack/plugins/monitoring/public/angular/index.ts
@@ -26,6 +26,7 @@ export class AngularApp {
pluginInitializerContext,
externalConfig,
triggersActionsUi,
+ usageCollection,
kibanaLegacy,
} = deps;
const app: IModule = localAppModule(deps);
@@ -42,6 +43,7 @@ export class AngularApp {
externalConfig,
kibanaLegacy,
triggersActionsUi,
+ usageCollection,
},
this.injector
);
diff --git a/x-pack/plugins/monitoring/public/components/page_loading/index.js b/x-pack/plugins/monitoring/public/components/page_loading/index.js
index c8a0404ec717b..6af11cd38c378 100644
--- a/x-pack/plugins/monitoring/public/components/page_loading/index.js
+++ b/x-pack/plugins/monitoring/public/components/page_loading/index.js
@@ -15,8 +15,9 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import './page_loading.scss';
+import { useTrackPageview } from '../../../../observability/public';
-export function PageLoading() {
+function PageLoadingUI() {
return (
@@ -45,3 +46,18 @@ export function PageLoading() {
);
}
+
+function PageLoadingTracking({ pageViewTitle }) {
+ const path = pageViewTitle.toLowerCase().replace(/-/g, '').replace(/\s+/g, '_');
+ useTrackPageview({ app: 'stack_monitoring', path });
+ useTrackPageview({ app: 'stack_monitoring', path, delay: 15000 });
+ return ;
+}
+
+export function PageLoading({ pageViewTitle }) {
+ if (pageViewTitle) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/x-pack/plugins/monitoring/public/components/setup_mode/enter_button.tsx b/x-pack/plugins/monitoring/public/components/setup_mode/enter_button.tsx
index e06113255c1ef..b47b51e664f5f 100644
--- a/x-pack/plugins/monitoring/public/components/setup_mode/enter_button.tsx
+++ b/x-pack/plugins/monitoring/public/components/setup_mode/enter_button.tsx
@@ -8,6 +8,8 @@ import React from 'react';
import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import './enter_button.scss';
+import { METRIC_TYPE, useUiTracker } from '../../../../observability/public';
+import { TELEMETRY_METRIC_BUTTON_CLICK } from '../../../common/constants';
export interface SetupModeEnterButtonProps {
enabled: boolean;
@@ -18,6 +20,7 @@ export const SetupModeEnterButton: React.FC = (
props: SetupModeEnterButtonProps
) => {
const [isLoading, setIsLoading] = React.useState(false);
+ const trackStat = useUiTracker({ app: 'stack_monitoring' });
if (!props.enabled) {
return null;
@@ -26,6 +29,10 @@ export const SetupModeEnterButton: React.FC = (
async function enterSetupMode() {
setIsLoading(true);
await props.toggleSetupMode(true);
+ trackStat({
+ metric: `${TELEMETRY_METRIC_BUTTON_CLICK}setupmode_enter`,
+ metricType: METRIC_TYPE.CLICK,
+ });
setIsLoading(false);
}
diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts
index 0f979e5637d68..488450bafd3a2 100644
--- a/x-pack/plugins/monitoring/public/legacy_shims.ts
+++ b/x-pack/plugins/monitoring/public/legacy_shims.ts
@@ -14,6 +14,7 @@ import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui
import { TypeRegistry } from '../../triggers_actions_ui/public/application/type_registry';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ActionTypeModel, AlertTypeModel } from '../../triggers_actions_ui/public/types';
+import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
interface BreadcrumbItem {
['data-test-subj']?: string;
@@ -59,13 +60,14 @@ export interface IShims {
) => Promise;
isCloud: boolean;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
+ usageCollection: UsageCollectionSetup;
}
export class Legacy {
private static _shims: IShims;
public static init(
- { core, data, isCloud, triggersActionsUi }: MonitoringStartPluginDependencies,
+ { core, data, isCloud, triggersActionsUi, usageCollection }: MonitoringStartPluginDependencies,
ngInjector: angular.auto.IInjectorService
) {
this._shims = {
@@ -119,6 +121,7 @@ export class Legacy {
}),
isCloud,
triggersActionsUi,
+ usageCollection,
};
}
diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx
index 3425e0ee2a818..3e555c843a0bb 100644
--- a/x-pack/plugins/monitoring/public/lib/setup_mode.tsx
+++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx
@@ -8,6 +8,7 @@ import React from 'react';
import { render } from 'react-dom';
import { get, includes } from 'lodash';
import { i18n } from '@kbn/i18n';
+import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
import { Legacy } from '../legacy_shims';
import { ajaxErrorHandlersProvider } from './ajax_error_handler';
import { SetupModeEnterButton } from '../components/setup_mode/enter_button';
@@ -179,8 +180,17 @@ export const setSetupModeMenuItem = () => {
const globalState = angularState.injector.get('globalState');
const enabled = !globalState.inSetupMode;
+ const services = {
+ usageCollection: Legacy.shims.usageCollection,
+ };
+ const I18nContext = Legacy.shims.I18nContext;
+
render(
- ,
+
+
+
+
+ ,
document.getElementById('setupModeNav')
);
};
diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts
index 05aa75f586241..087e7acc4c703 100644
--- a/x-pack/plugins/monitoring/public/plugin.ts
+++ b/x-pack/plugins/monitoring/public/plugin.ts
@@ -13,6 +13,7 @@ import {
Plugin,
PluginInitializerContext,
} from 'kibana/public';
+import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
import {
FeatureCatalogueCategory,
HomePublicPluginSetup,
@@ -28,6 +29,7 @@ interface MonitoringSetupPluginDependencies {
home?: HomePublicPluginSetup;
cloud?: { isCloudEnabled: boolean };
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
+ usageCollection: UsageCollectionSetup;
}
export class MonitoringPlugin
@@ -93,6 +95,7 @@ export class MonitoringPlugin
pluginInitializerContext: this.initializerContext,
externalConfig: this.getExternalConfig(),
triggersActionsUi: plugins.triggers_actions_ui,
+ usageCollection: plugins.usageCollection,
};
pluginsStart.kibanaLegacy.loadFontAwesome();
diff --git a/x-pack/plugins/monitoring/public/types.ts b/x-pack/plugins/monitoring/public/types.ts
index f911af2db8c58..238af7276d586 100644
--- a/x-pack/plugins/monitoring/public/types.ts
+++ b/x-pack/plugins/monitoring/public/types.ts
@@ -9,6 +9,7 @@ import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public';
import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public';
+import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
export { MonitoringConfig } from '../server';
@@ -23,4 +24,5 @@ export interface MonitoringStartPluginDependencies {
pluginInitializerContext: PluginInitializerContext;
externalConfig: Array | Array>;
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
+ usageCollection: UsageCollectionSetup;
}
diff --git a/x-pack/plugins/monitoring/public/views/apm/instance/index.js b/x-pack/plugins/monitoring/public/views/apm/instance/index.js
index 752c46b18bfb4..752128782194e 100644
--- a/x-pack/plugins/monitoring/public/views/apm/instance/index.js
+++ b/x-pack/plugins/monitoring/public/views/apm/instance/index.js
@@ -44,6 +44,7 @@ uiRoutes.when('/apm/instances/:uuid', {
apm: 'APM server',
},
}),
+ telemetryPageViewTitle: 'apm_server_instance',
api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm/${$route.current.params.uuid}`,
defaultData: {},
reactNodeId: 'apmInstanceReact',
@@ -63,21 +64,16 @@ uiRoutes.when('/apm/instances/:uuid', {
})
);
title($scope.cluster, `APM server - ${get(data, 'apmSummary.name')}`);
- this.renderReact(data);
+ this.renderReact(
+
+ );
}
);
}
-
- renderReact(data) {
- const component = (
-
- );
- super.renderReact(component);
- }
},
});
diff --git a/x-pack/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/plugins/monitoring/public/views/apm/instances/index.js
index 764e13ccfea8d..1f5b089ea748e 100644
--- a/x-pack/plugins/monitoring/public/views/apm/instances/index.js
+++ b/x-pack/plugins/monitoring/public/views/apm/instances/index.js
@@ -55,37 +55,33 @@ uiRoutes.when('/apm/instances', {
$scope.$watch(
() => this.data,
(data) => {
- this.renderReact(data);
- }
- );
- }
+ const { pagination, sorting, onTableChange } = this;
- renderReact(data) {
- const { pagination, sorting, onTableChange } = this;
-
- const component = (
- (
-
- {flyoutComponent}
-
- {bottomBarComponent}
-
- )}
- />
+ const component = (
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+ );
+ this.renderReact(component);
+ }
);
- super.renderReact(component);
}
},
});
diff --git a/x-pack/plugins/monitoring/public/views/apm/overview/index.js b/x-pack/plugins/monitoring/public/views/apm/overview/index.js
index 670acaeacce03..544fae39ee79d 100644
--- a/x-pack/plugins/monitoring/public/views/apm/overview/index.js
+++ b/x-pack/plugins/monitoring/public/views/apm/overview/index.js
@@ -47,14 +47,11 @@ uiRoutes.when('/apm', {
$scope.$watch(
() => this.data,
(data) => {
- this.renderReact(data);
+ this.renderReact(
+
+ );
}
);
}
-
- renderReact(data) {
- const component = ;
- super.renderReact(component);
- }
},
});
diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js
index 9c5aef950fc2b..0eb40c8dd5963 100644
--- a/x-pack/plugins/monitoring/public/views/base_controller.js
+++ b/x-pack/plugins/monitoring/public/views/base_controller.js
@@ -13,6 +13,7 @@ import { Legacy } from '../legacy_shims';
import { PromiseWithCancel } from '../../common/cancel_promise';
import { SetupModeFeature } from '../../common/enums';
import { updateSetupModeData, isSetupModeFeatureEnabled } from '../lib/setup_mode';
+import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public';
/**
* Given a timezone, this function will calculate the offset in milliseconds
@@ -89,6 +90,7 @@ export class MonitoringViewBaseController {
options = {},
alerts = { shouldFetch: false, options: {} },
fetchDataImmediately = true,
+ telemetryPageViewTitle = '',
}) {
const titleService = $injector.get('title');
const $executor = $injector.get('$executor');
@@ -102,6 +104,7 @@ export class MonitoringViewBaseController {
$scope.pageData = this.data = { ...defaultData };
this._isDataInitialized = false;
this.reactNodeId = reactNodeId;
+ this.telemetryPageViewTitle = telemetryPageViewTitle || title;
let deferTimer;
let zoomInLevel = 0;
@@ -207,6 +210,8 @@ export class MonitoringViewBaseController {
deferTimer = setTimeout(() => addPopstateHandler(), 10);
};
+ // Render loading state
+ this.renderReact(null, true);
fetchDataImmediately && this.updateData();
});
@@ -228,15 +233,26 @@ export class MonitoringViewBaseController {
this.setTitle = (title) => titleService($scope.cluster, title);
}
- renderReact(component) {
+ renderReact(component, trackPageView = false) {
const renderElement = document.getElementById(this.reactNodeId);
if (!renderElement) {
console.warn(`"#${this.reactNodeId}" element has not been added to the DOM yet`);
return;
}
+ const services = {
+ usageCollection: Legacy.shims.usageCollection,
+ };
const I18nContext = Legacy.shims.I18nContext;
const wrappedComponent = (
- {!this._isDataInitialized ? : component}
+
+
+ {!this._isDataInitialized ? (
+
+ ) : (
+ component
+ )}
+
+
);
render(wrappedComponent, renderElement);
}
diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/index.js b/x-pack/plugins/monitoring/public/views/beats/beat/index.js
index 70a9f33b4f03d..6cffae2479128 100644
--- a/x-pack/plugins/monitoring/public/views/beats/beat/index.js
+++ b/x-pack/plugins/monitoring/public/views/beats/beat/index.js
@@ -47,6 +47,7 @@ uiRoutes.when('/beats/beat/:beatUuid', {
beatName: pageData.summary.name,
},
}),
+ telemetryPageViewTitle: 'beats_instance',
getPageData,
$scope,
$injector,
diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/plugins/monitoring/public/views/beats/listing/index.js
index 004f89adf0467..a1b2231901c54 100644
--- a/x-pack/plugins/monitoring/public/views/beats/listing/index.js
+++ b/x-pack/plugins/monitoring/public/views/beats/listing/index.js
@@ -40,6 +40,7 @@ uiRoutes.when('/beats/beats', {
pageTitle: i18n.translate('xpack.monitoring.beats.listing.pageTitle', {
defaultMessage: 'Beats listing',
}),
+ telemetryPageViewTitle: 'beats_listing',
storageKey: 'beats.beats',
getPageData,
reactNodeId: 'monitoringBeatsInstancesApp',
@@ -51,9 +52,6 @@ uiRoutes.when('/beats/beats', {
this.scope = $scope;
this.injector = $injector;
- //Bypassing super.updateData, since this controller loads its own data
- this._isDataInitialized = true;
-
$scope.$watch(
() => this.data,
() => this.renderComponent()
diff --git a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js
index b1e850ef3a905..dd984f559d469 100644
--- a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js
+++ b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js
@@ -52,6 +52,7 @@ uiRoutes
$scope,
$injector,
reactNodeId: 'monitoringClusterListingApp',
+ telemetryPageViewTitle: 'cluster_listing',
});
const $route = $injector.get('$route');
diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js
index 91537d5c77ba4..6f27a12223b26 100644
--- a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js
+++ b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js
@@ -54,6 +54,7 @@ uiRoutes.when('/overview', {
alerts: {
shouldFetch: true,
},
+ telemetryPageViewTitle: 'cluster_overview',
});
this.init = () => this.renderReact(null);
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js
index 32b3546510f91..6569340785736 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js
@@ -42,13 +42,12 @@ uiRoutes.when('/elasticsearch/ccr', {
$scope.$watch(
() => this.data,
(data) => {
- this.renderReact(data);
+ if (!data) {
+ return;
+ }
+ this.renderReact();
}
);
-
- this.renderReact = ({ data }) => {
- super.renderReact();
- };
}
},
});
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js
index e60a29ea8a5ed..33a2d27f39856 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js
@@ -48,6 +48,10 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', {
$scope.$watch(
() => this.data,
(data) => {
+ if (!data) {
+ return;
+ }
+
this.setPageTitle(
i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.pageTitle', {
defaultMessage: 'Elasticsearch Ccr Shard - Index: {followerIndex} Shard: {shardId}',
@@ -57,13 +61,10 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', {
},
})
);
- this.renderReact(data);
+
+ this.renderReact();
}
);
-
- this.renderReact = (props) => {
- super.renderReact();
- };
}
},
});
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js
index f4b0f0789bae1..cfc36e360709d 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js
@@ -64,6 +64,7 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', {
indexName,
},
}),
+ telemetryPageViewTitle: 'elasticsearch_index_advanced',
defaultData: {},
getPageData,
reactNodeId: 'monitoringElasticsearchAdvancedIndexApp',
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js
index a7ca4b5b87ab0..76628a0a02e42 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js
@@ -66,6 +66,7 @@ uiRoutes.when('/elasticsearch/indices/:index', {
indexName,
},
}),
+ telemetryPageViewTitle: 'elasticsearch_index',
pageTitle: i18n.translate('xpack.monitoring.elasticsearch.indices.overview.pageTitle', {
defaultMessage: 'Index: {indexName}',
values: {
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js
index 911f7146d7282..490bd02db42b7 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js
@@ -67,23 +67,24 @@ uiRoutes.when('/elasticsearch/indices', {
$scope.$watch(
() => this.data,
(data) => {
- this.renderReact(data);
+ if (!data) {
+ return;
+ }
+
+ const { clusterStatus, indices } = data;
+ this.renderReact(
+
+ );
}
);
-
- this.renderReact = ({ clusterStatus, indices }) => {
- super.renderReact(
-
- );
- };
}
},
});
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
index b5d498950ef07..5c4b4d28b93cb 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js
@@ -61,6 +61,7 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', {
defaultData: {},
getPageData,
reactNodeId: 'monitoringElasticsearchAdvancedNodeApp',
+ telemetryPageViewTitle: 'elasticsearch_node_advanced',
$scope,
$injector,
alerts: {
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
index faff1e09aff03..b4b3c7ca55303 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js
@@ -36,6 +36,13 @@ uiRoutes.when('/elasticsearch/nodes/:node', {
const nodeName = $route.current.params.node;
super({
+ title: i18n.translate('xpack.monitoring.elasticsearch.node.overview.routeTitle', {
+ defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Overview',
+ values: {
+ nodeName,
+ },
+ }),
+ telemetryPageViewTitle: 'elasticsearch_node',
defaultData: {},
getPageData,
reactNodeId: 'monitoringElasticsearchNodeApp',
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
index 2a1172105b073..33584f802a56e 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js
@@ -95,38 +95,41 @@ uiRoutes.when('/elasticsearch/nodes', {
$scope.$watch(
() => this.data,
- () => this.renderReact(this.data || {})
- );
+ (data) => {
+ if (!data) {
+ return;
+ }
- this.renderReact = ({ clusterStatus, nodes, totalNodeCount }) => {
- const pagination = {
- ...this.pagination,
- totalItemCount: totalNodeCount,
- };
+ const { clusterStatus, nodes, totalNodeCount } = data;
+ const pagination = {
+ ...this.pagination,
+ totalItemCount: totalNodeCount,
+ };
- super.renderReact(
- (
-
- {flyoutComponent}
-
- {bottomBarComponent}
-
- )}
- />
- );
- };
+ this.renderReact(
+ (
+
+ {flyoutComponent}
+
+ {bottomBarComponent}
+
+ )}
+ />
+ );
+ }
+ );
}
},
});
diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js
index 5dca8a2dbd907..f383b36bb3524 100644
--- a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js
+++ b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js
@@ -78,7 +78,7 @@ export class ElasticsearchOverviewController extends MonitoringViewBaseControlle
renderReact(data, cluster) {
// All data needs to originate in this view, and get passed as a prop to the components, for statelessness
- const { clusterStatus, metrics, shardActivity, logs } = data;
+ const { clusterStatus, metrics, shardActivity, logs } = data || {};
const shardActivityData = shardActivity && this.filterShardActivityData(shardActivity); // no filter on data = null
const component = (
({
+ fetchClusters: jest.fn().mockImplementation(() => {
+ return [
+ {
+ clusterUuid: '1abc',
+ clusterName: 'unitTesting',
+ },
+ ];
+ }),
+}));
+
+jest.mock('./lib/get_stack_products_usage', () => ({
+ getStackProductsUsage: jest.fn().mockImplementation(() => {
+ return {
+ elasticsearch: {
+ count: 5,
+ enabled: true,
+ metricbeatUsed: true,
+ },
+ kibana: {
+ count: 2,
+ enabled: true,
+ metricbeatUsed: false,
+ },
+ logstash: {
+ count: 0,
+ enabled: false,
+ metricbeatUsed: false,
+ },
+ beats: {
+ count: 1,
+ enabled: true,
+ metricbeatUsed: false,
+ },
+ apm: {
+ count: 1,
+ enabled: true,
+ metricbeatUsed: true,
+ },
+ };
+ }),
+}));
+
+jest.mock('./lib/fetch_license_type', () => ({
+ fetchLicenseType: jest.fn().mockImplementation(() => {
+ return 'trial';
+ }),
+}));
+
+describe('getMonitoringUsageCollector', () => {
+ const callCluster = jest.fn();
+ const config: any = {
+ ui: {
+ ccs: {
+ enabled: true,
+ },
+ },
+ };
+
+ it('should be configured correctly', async () => {
+ const usageCollection: any = {
+ makeUsageCollector: jest.fn(),
+ };
+ await getMonitoringUsageCollector(usageCollection, config, callCluster);
+
+ const mock = (usageCollection.makeUsageCollector as jest.Mock).mock;
+
+ const args = mock.calls[0];
+ expect(args[0].type).toBe('monitoring');
+ expect(typeof args[0].isReady).toBe('function');
+ expect(args[0].schema).toStrictEqual({
+ hasMonitoringData: { type: 'boolean' },
+ clusters: {
+ license: { type: 'keyword' },
+ clusterUuid: { type: 'keyword' },
+ metricbeatUsed: { type: 'boolean' },
+ elasticsearch: {
+ enabled: { type: 'boolean' },
+ count: { type: 'long' },
+ metricbeatUsed: { type: 'boolean' },
+ },
+ kibana: {
+ enabled: { type: 'boolean' },
+ count: { type: 'long' },
+ metricbeatUsed: { type: 'boolean' },
+ },
+ logstash: {
+ enabled: { type: 'boolean' },
+ count: { type: 'long' },
+ metricbeatUsed: { type: 'boolean' },
+ },
+ beats: {
+ enabled: { type: 'boolean' },
+ count: { type: 'long' },
+ metricbeatUsed: { type: 'boolean' },
+ },
+ apm: {
+ enabled: { type: 'boolean' },
+ count: { type: 'long' },
+ metricbeatUsed: { type: 'boolean' },
+ },
+ },
+ });
+ });
+
+ it('should fetch usage data', async () => {
+ const usageCollection: any = {
+ makeUsageCollector: jest.fn(),
+ };
+
+ await getMonitoringUsageCollector(usageCollection, config, callCluster);
+ const mock = (usageCollection.makeUsageCollector as jest.Mock).mock;
+ const args = mock.calls[0];
+
+ const result = await args[0].fetch();
+ expect(result).toStrictEqual({
+ hasMonitoringData: true,
+ clusters: [
+ {
+ clusterUuid: '1abc',
+ license: 'trial',
+ elasticsearch: { count: 5, enabled: true, metricbeatUsed: true },
+ kibana: { count: 2, enabled: true, metricbeatUsed: false },
+ logstash: { count: 0, enabled: false, metricbeatUsed: false },
+ beats: { count: 1, enabled: true, metricbeatUsed: false },
+ apm: { count: 1, enabled: true, metricbeatUsed: true },
+ metricbeatUsed: true,
+ },
+ ],
+ });
+ });
+
+ it('should handle no monitoring data', async () => {
+ const usageCollection: any = {
+ makeUsageCollector: jest.fn(),
+ };
+
+ await getMonitoringUsageCollector(usageCollection, config, callCluster);
+ const mock = (usageCollection.makeUsageCollector as jest.Mock).mock;
+ const args = mock.calls[0];
+
+ (fetchClusters as jest.Mock).mockImplementation(() => {
+ return [];
+ });
+
+ const result = await args[0].fetch();
+ expect(result).toStrictEqual({
+ hasMonitoringData: false,
+ clusters: [],
+ });
+ });
+});
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts
new file mode 100644
index 0000000000000..b743a5f8e0b4f
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts
@@ -0,0 +1,128 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { LegacyAPICaller } from 'src/core/server';
+import { MonitoringConfig } from '../../config';
+import { fetchAvailableCcs } from '../../lib/alerts/fetch_available_ccs';
+import { getStackProductsUsage } from './lib/get_stack_products_usage';
+import { fetchLicenseType } from './lib/fetch_license_type';
+import { MonitoringUsage, StackProductUsage, MonitoringClusterStackProductUsage } from './types';
+import { INDEX_PATTERN_ELASTICSEARCH } from '../../../common/constants';
+import { getCcsIndexPattern } from '../../lib/alerts/get_ccs_index_pattern';
+import { fetchClusters } from '../../lib/alerts/fetch_clusters';
+
+export function getMonitoringUsageCollector(
+ usageCollection: UsageCollectionSetup,
+ config: MonitoringConfig,
+ callCluster: LegacyAPICaller
+) {
+ return usageCollection.makeUsageCollector({
+ type: 'monitoring',
+ isReady: () => true,
+ schema: {
+ hasMonitoringData: {
+ type: 'boolean',
+ },
+ clusters: {
+ license: {
+ type: 'keyword',
+ },
+ clusterUuid: {
+ type: 'keyword',
+ },
+ metricbeatUsed: {
+ type: 'boolean',
+ },
+ elasticsearch: {
+ enabled: {
+ type: 'boolean',
+ },
+ count: {
+ type: 'long',
+ },
+ metricbeatUsed: {
+ type: 'boolean',
+ },
+ },
+ kibana: {
+ enabled: {
+ type: 'boolean',
+ },
+ count: {
+ type: 'long',
+ },
+ metricbeatUsed: {
+ type: 'boolean',
+ },
+ },
+ logstash: {
+ enabled: {
+ type: 'boolean',
+ },
+ count: {
+ type: 'long',
+ },
+ metricbeatUsed: {
+ type: 'boolean',
+ },
+ },
+ beats: {
+ enabled: {
+ type: 'boolean',
+ },
+ count: {
+ type: 'long',
+ },
+ metricbeatUsed: {
+ type: 'boolean',
+ },
+ },
+ apm: {
+ enabled: {
+ type: 'boolean',
+ },
+ count: {
+ type: 'long',
+ },
+ metricbeatUsed: {
+ type: 'boolean',
+ },
+ },
+ },
+ },
+ fetch: async () => {
+ const usageClusters: MonitoringClusterStackProductUsage[] = [];
+ const availableCcs = config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : [];
+ const elasticsearchIndex = getCcsIndexPattern(INDEX_PATTERN_ELASTICSEARCH, availableCcs);
+ const clusters = await fetchClusters(callCluster, elasticsearchIndex);
+ for (const cluster of clusters) {
+ const license = await fetchLicenseType(callCluster, availableCcs, cluster.clusterUuid);
+ const stackProducts = await getStackProductsUsage(
+ config,
+ callCluster,
+ availableCcs,
+ cluster.clusterUuid
+ );
+ usageClusters.push({
+ clusterUuid: cluster.clusterUuid,
+ license,
+ metricbeatUsed: Object.values(stackProducts).some(
+ (_usage: StackProductUsage) => _usage.metricbeatUsed
+ ),
+ ...stackProducts,
+ });
+ }
+
+ const usage = {
+ hasMonitoringData: usageClusters.length > 0,
+ clusters: usageClusters,
+ };
+
+ return usage;
+ },
+ });
+}
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts
index aa4853ab226f4..47ad78b29962c 100644
--- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/index.ts
@@ -4,15 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { LegacyAPICaller } from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getSettingsCollector } from './get_settings_collector';
+import { getMonitoringUsageCollector } from './get_usage_collector';
import { MonitoringConfig } from '../../config';
export { KibanaSettingsCollector } from './get_settings_collector';
export function registerCollectors(
usageCollection: UsageCollectionSetup,
- config: MonitoringConfig
+ config: MonitoringConfig,
+ callCluster: LegacyAPICaller
) {
usageCollection.registerCollector(getSettingsCollector(usageCollection, config));
+ usageCollection.registerCollector(
+ getMonitoringUsageCollector(usageCollection, config, callCluster)
+ );
}
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.test.ts
new file mode 100644
index 0000000000000..85fc0eb8dc6b2
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.test.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { fetchESUsage } from './fetch_es_usage';
+
+describe('fetchESUsage', () => {
+ const clusterUuid = '1abcde2';
+ const index = '.monitoring-es-*';
+ const callCluster = jest.fn().mockImplementation(() => ({
+ hits: {
+ hits: [
+ {
+ _source: {
+ cluster_stats: {
+ nodes: {
+ count: {
+ total: 10,
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ aggregations: {
+ indices: {
+ buckets: [
+ {
+ key: '.monitoring-es-2',
+ },
+ ],
+ },
+ },
+ }));
+ const config: any = {};
+
+ it('should return usage data for Elasticsearch', async () => {
+ const result = await fetchESUsage(config, callCluster, clusterUuid, index);
+ expect(result).toStrictEqual({
+ count: 10,
+ enabled: true,
+ metricbeatUsed: false,
+ });
+ });
+
+ it('should handle some indices coming from Metricbeat', async () => {
+ const customCallCluster = jest.fn().mockImplementation(() => ({
+ hits: {
+ hits: [
+ {
+ _source: {
+ cluster_stats: {
+ nodes: {
+ count: {
+ total: 10,
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ aggregations: {
+ indices: {
+ buckets: [
+ {
+ key: '.monitoring-es-mb-2',
+ },
+ ],
+ },
+ },
+ }));
+ const result = await fetchESUsage(config, customCallCluster, clusterUuid, index);
+ expect(result).toStrictEqual({
+ count: 10,
+ enabled: true,
+ metricbeatUsed: true,
+ });
+ });
+
+ it('should handle no monitoring data', async () => {
+ const customCallCluster = jest.fn().mockImplementation(() => ({
+ hits: {
+ hits: [],
+ },
+ }));
+ const result = await fetchESUsage(config, customCallCluster, clusterUuid, index);
+ expect(result).toStrictEqual({
+ count: 0,
+ enabled: false,
+ metricbeatUsed: false,
+ });
+ });
+});
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts
new file mode 100644
index 0000000000000..de0a1b8f99d96
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts
@@ -0,0 +1,122 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { LegacyAPICaller } from 'src/core/server';
+import { get } from 'lodash';
+import { MonitoringConfig } from '../../../config';
+import { StackProductUsage } from '../types';
+
+interface ESResponse {
+ hits: {
+ hits: ESResponseHits[];
+ };
+ aggregations: {
+ indices: {
+ buckets: ESIndicesBucket;
+ };
+ };
+}
+
+interface ESIndicesBucket {
+ key: string;
+}
+
+interface ESResponseHits {
+ _source: ClusterStats;
+}
+
+interface ClusterStats {
+ cluster_stats: {
+ nodes: {
+ count: {
+ total: number;
+ };
+ };
+ };
+ version: string;
+}
+
+export async function fetchESUsage(
+ config: MonitoringConfig,
+ callCluster: LegacyAPICaller,
+ clusterUuid: string,
+ index: string
+): Promise {
+ const params = {
+ index,
+ size: 1,
+ ignoreUnavailable: true,
+ filterPath: [
+ 'hits.hits._source.cluster_stats.nodes.count.total',
+ 'aggregations.indices.buckets',
+ ],
+ body: {
+ sort: [
+ {
+ timestamp: {
+ order: 'desc',
+ },
+ },
+ ],
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ type: {
+ value: 'cluster_stats',
+ },
+ },
+ },
+ {
+ term: {
+ cluster_uuid: {
+ value: clusterUuid,
+ },
+ },
+ },
+ {
+ range: {
+ timestamp: {
+ gte: 'now-1h',
+ },
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ indices: {
+ terms: {
+ field: '_index',
+ size: 2,
+ },
+ },
+ },
+ },
+ };
+
+ const response = await callCluster('search', params);
+ const esResponse = response as ESResponse;
+ if (esResponse.hits.hits.length === 0) {
+ return {
+ count: 0,
+ enabled: false,
+ metricbeatUsed: false,
+ };
+ }
+
+ const hit = esResponse.hits.hits[0]._source;
+ const count = hit.cluster_stats.nodes.count.total;
+ const buckets = get(esResponse, 'aggregations.indices.buckets', []) as ESIndicesBucket[];
+ const metricbeatUsed = Boolean(buckets.find((indexBucket) => indexBucket.key.includes('-mb-')));
+
+ return {
+ count,
+ enabled: true,
+ metricbeatUsed,
+ };
+}
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts
new file mode 100644
index 0000000000000..1026dc339e29e
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { fetchLicenseType } from './fetch_license_type';
+
+describe('fetchLicenseType', () => {
+ const clusterUuid = '1abcde2';
+ const availableCcs: string[] = [];
+ const callCluster = jest.fn().mockImplementation(() => ({
+ hits: {
+ hits: [
+ {
+ _source: {
+ license: {
+ type: 'trial',
+ },
+ },
+ },
+ ],
+ },
+ }));
+
+ it('should get the license type', async () => {
+ const result = await fetchLicenseType(callCluster, availableCcs, clusterUuid);
+ expect(result).toStrictEqual('trial');
+ });
+
+ it('should handle no license data', async () => {
+ const customCallCluster = jest.fn().mockImplementation(() => ({
+ hits: {
+ hits: [],
+ },
+ }));
+ const result = await fetchLicenseType(customCallCluster, availableCcs, clusterUuid);
+ expect(result).toStrictEqual(null);
+ });
+});
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts
new file mode 100644
index 0000000000000..f7b8b72637b1f
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get } from 'lodash';
+import { LegacyAPICaller } from 'src/core/server';
+import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../common/constants';
+import { getCcsIndexPattern } from '../../../lib/alerts/get_ccs_index_pattern';
+
+export async function fetchLicenseType(
+ callCluster: LegacyAPICaller,
+ availableCcs: string[],
+ clusterUuid: string
+) {
+ let index = INDEX_PATTERN_ELASTICSEARCH;
+ if (availableCcs) {
+ index = getCcsIndexPattern(index, availableCcs);
+ }
+ const params = {
+ index,
+ filterPath: ['hits.hits._source.license'],
+ body: {
+ size: 1,
+ sort: [
+ {
+ timestamp: {
+ order: 'desc',
+ },
+ },
+ ],
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ cluster_uuid: {
+ value: clusterUuid,
+ },
+ },
+ },
+ {
+ term: {
+ type: {
+ value: 'cluster_stats',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ };
+ const response = await callCluster('search', params);
+ return get(response, 'hits.hits[0]._source.license.type', null);
+}
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.test.ts
new file mode 100644
index 0000000000000..9377dee2f31f9
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.test.ts
@@ -0,0 +1,117 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { fetchStackProductUsage } from './fetch_stack_product_usage';
+
+describe('fetchStackProductUsage', () => {
+ const clusterUuid = '1abcde2';
+ const config: any = {
+ ui: {
+ max_bucket_size: 10000,
+ },
+ };
+
+ it('should use appropiate query parameters', async () => {
+ const callCluster = jest.fn();
+ await fetchStackProductUsage(
+ config,
+ callCluster,
+ clusterUuid,
+ '.monitoring-kibana-*',
+ 'kibana_stats',
+ 'kibana_stats.kibana.uuid',
+ [
+ {
+ term: {
+ type: {
+ value: 'foo',
+ },
+ },
+ },
+ ]
+ );
+ const params = callCluster.mock.calls[0][1];
+ expect(params.body.query.bool.must[0].term.type.value).toBe('kibana_stats');
+ expect(params.body.query.bool.must[1].term.cluster_uuid.value).toBe(clusterUuid);
+ expect(params.body.query.bool.must[2].range.timestamp.gte).toBe('now-1h');
+ expect(params.body.query.bool.must[3].term.type.value).toBe('foo');
+ });
+
+ it('should get the usage data', async () => {
+ const callCluster = jest.fn().mockImplementation(() => ({
+ aggregations: {
+ uuids: {
+ buckets: [
+ {
+ key: 'sadfsdf',
+ indices: {
+ buckets: [
+ {
+ key: '.monitoring-kibana-8',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ }));
+
+ const result = await fetchStackProductUsage(
+ config,
+ callCluster,
+ clusterUuid,
+ '.monitoring-kibana-*',
+ 'kibana_stats',
+ 'kibana_stats.kibana.uuid'
+ );
+
+ expect(result).toStrictEqual({
+ count: 1,
+ enabled: true,
+ metricbeatUsed: false,
+ });
+ });
+
+ it('should handle both collection types', async () => {
+ const callCluster = jest.fn().mockImplementation(() => ({
+ aggregations: {
+ uuids: {
+ buckets: [
+ {
+ key: 'sadfsdf',
+ indices: {
+ buckets: [
+ {
+ key: '.monitoring-kibana-8',
+ },
+ {
+ key: '.monitoring-kibana-mb-8',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+ }));
+
+ const result = await fetchStackProductUsage(
+ config,
+ callCluster,
+ clusterUuid,
+ '.monitoring-kibana-*',
+ 'kibana_stats',
+ 'kibana_stats.kibana.uuid'
+ );
+
+ expect(result).toStrictEqual({
+ count: 1,
+ enabled: true,
+ metricbeatUsed: true,
+ });
+ });
+});
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts
new file mode 100644
index 0000000000000..df18b28d36c61
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts
@@ -0,0 +1,111 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get } from 'lodash';
+import { LegacyAPICaller } from 'src/core/server';
+import { MonitoringConfig } from '../../../config';
+// @ts-ignore
+import { prefixIndexPattern } from '../../../lib/ccs_utils';
+import { StackProductUsage } from '../types';
+
+interface ESResponse {
+ aggregations?: {
+ uuids: {
+ buckets: UuidBucket[];
+ };
+ };
+}
+
+interface UuidBucket {
+ key: string;
+ indices: {
+ buckets: KeyBucket[];
+ };
+}
+
+interface KeyBucket {
+ key: string;
+}
+
+export async function fetchStackProductUsage(
+ config: MonitoringConfig,
+ callCluster: LegacyAPICaller,
+ clusterUuid: string,
+ index: string,
+ type: string,
+ uuidPath: string,
+ filters: any[] = []
+): Promise {
+ const size = config.ui.max_bucket_size;
+ const params = {
+ index,
+ size: 0,
+ ignoreUnavailable: true,
+ filterPath: ['aggregations.uuids.buckets'],
+ body: {
+ query: {
+ bool: {
+ must: [
+ {
+ term: {
+ type: {
+ value: type,
+ },
+ },
+ },
+ {
+ term: {
+ cluster_uuid: {
+ value: clusterUuid,
+ },
+ },
+ },
+ {
+ range: {
+ timestamp: {
+ gte: 'now-1h',
+ },
+ },
+ },
+ ...filters,
+ ],
+ },
+ },
+ aggs: {
+ uuids: {
+ terms: {
+ field: uuidPath,
+ size,
+ },
+ aggs: {
+ indices: {
+ terms: {
+ field: '_index',
+ size: 2,
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const response = (await callCluster('search', params)) as ESResponse;
+ const uuidBuckets = get(response, 'aggregations.uuids.buckets', []) as UuidBucket[];
+ const count = uuidBuckets.length;
+ const metricbeatUsed = Boolean(
+ uuidBuckets.find((uuidBucket) =>
+ (get(uuidBucket, 'indices.buckets', []) as KeyBucket[]).find((indexBucket) =>
+ indexBucket.key.includes('-mb-')
+ )
+ )
+ );
+ return {
+ count,
+ enabled: count > 0,
+ metricbeatUsed,
+ };
+}
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.test.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.test.ts
new file mode 100644
index 0000000000000..ca8def84432ea
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.test.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getStackProductsUsage } from './get_stack_products_usage';
+
+describe('getStackProductsUsage', () => {
+ const config: any = {
+ ui: {
+ max_bucket_size: 10000,
+ },
+ };
+ const clusterUuid = '1abcde2';
+ const availableCcs: string[] = [];
+ const callCluster = jest.fn().mockImplementation(() => ({
+ hits: {
+ hits: [],
+ },
+ }));
+
+ it('should get all stack products', async () => {
+ const result = await getStackProductsUsage(config, callCluster, availableCcs, clusterUuid);
+ expect(result.elasticsearch).toBeDefined();
+ expect(result.kibana).toBeDefined();
+ expect(result.logstash).toBeDefined();
+ expect(result.beats).toBeDefined();
+ expect(result.apm).toBeDefined();
+ });
+});
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts
new file mode 100644
index 0000000000000..ffa15168d5c8a
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { LegacyAPICaller } from 'src/core/server';
+import { MonitoringClusterStackProductUsage } from '../types';
+import { fetchESUsage } from './fetch_es_usage';
+import { MonitoringConfig } from '../../../config';
+// @ts-ignore
+import { getIndexPatterns } from '../../../lib/cluster/get_index_patterns';
+// @ts-ignore
+import { prefixIndexPattern } from '../../../lib/ccs_utils';
+import {
+ INDEX_PATTERN_ELASTICSEARCH,
+ INDEX_PATTERN_KIBANA,
+ INDEX_PATTERN_LOGSTASH,
+ INDEX_PATTERN_BEATS,
+} from '../../../../common/constants';
+import { fetchStackProductUsage } from './fetch_stack_product_usage';
+import { getCcsIndexPattern } from '../../../lib/alerts/get_ccs_index_pattern';
+
+export const getStackProductsUsage = async (
+ config: MonitoringConfig,
+ callCluster: LegacyAPICaller,
+ availableCcs: string[],
+ clusterUuid: string
+): Promise<
+ Pick<
+ MonitoringClusterStackProductUsage,
+ 'elasticsearch' | 'kibana' | 'logstash' | 'beats' | 'apm'
+ >
+> => {
+ const elasticsearchIndex = getCcsIndexPattern(INDEX_PATTERN_ELASTICSEARCH, availableCcs);
+ const kibanaIndex = getCcsIndexPattern(INDEX_PATTERN_KIBANA, availableCcs);
+ const logstashIndex = getCcsIndexPattern(INDEX_PATTERN_LOGSTASH, availableCcs);
+ const beatsIndex = getCcsIndexPattern(INDEX_PATTERN_BEATS, availableCcs);
+ const [elasticsearch, kibana, logstash, beats, apm] = await Promise.all([
+ fetchESUsage(config, callCluster, clusterUuid, elasticsearchIndex),
+ fetchStackProductUsage(
+ config,
+ callCluster,
+ clusterUuid,
+ kibanaIndex,
+ 'kibana_stats',
+ 'kibana_stats.kibana.uuid'
+ ),
+ fetchStackProductUsage(
+ config,
+ callCluster,
+ clusterUuid,
+ logstashIndex,
+ 'logstash_stats',
+ 'logstash_stats.logstash.uuid'
+ ),
+ fetchStackProductUsage(
+ config,
+ callCluster,
+ clusterUuid,
+ beatsIndex,
+ 'beats_stats',
+ 'beats_stats.beat.uuid'
+ ),
+ fetchStackProductUsage(
+ config,
+ callCluster,
+ clusterUuid,
+ beatsIndex,
+ 'beats_stats',
+ 'beats_stats.beat.uuid',
+ [{ term: { 'beats_stats.beat.type': 'apm-server' } }]
+ ),
+ ]);
+
+ return { elasticsearch, kibana, logstash, beats, apm };
+};
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/types.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/types.ts
new file mode 100644
index 0000000000000..c8e0eeea815e1
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/types.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface MonitoringUsage {
+ hasMonitoringData: boolean;
+ clusters: MonitoringClusterStackProductUsage[];
+}
+
+export interface MonitoringClusterStackProductUsage {
+ clusterUuid: string;
+ license: string;
+ metricbeatUsed: boolean;
+ elasticsearch: StackProductUsage;
+ logstash: StackProductUsage;
+ kibana: StackProductUsage;
+ beats: StackProductUsage;
+ apm: StackProductUsage;
+}
+
+export interface StackProductUsage {
+ count: number;
+ enabled: boolean;
+ metricbeatUsed: boolean;
+}
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts b/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts
index 1907d2b4b3401..f16e463b508fb 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
export function getCcsIndexPattern(indexPattern: string, remotes: string[]): string {
+ if (remotes.length === 0) {
+ return indexPattern;
+ }
const patternsToAdd = [];
for (const index of indexPattern.split(',')) {
for (const remote of remotes) {
diff --git a/x-pack/plugins/monitoring/server/plugin.test.ts b/x-pack/plugins/monitoring/server/plugin.test.ts
index a2520593c436d..727ada52e6e3d 100644
--- a/x-pack/plugins/monitoring/server/plugin.test.ts
+++ b/x-pack/plugins/monitoring/server/plugin.test.ts
@@ -14,7 +14,9 @@ jest.mock('rxjs', () => ({
}));
jest.mock('./es_client/instantiate_client', () => ({
- instantiateClient: jest.fn(),
+ instantiateClient: jest.fn().mockImplementation(() => ({
+ cluster: {},
+ })),
}));
jest.mock('./license_service', () => ({
@@ -25,6 +27,10 @@ jest.mock('./license_service', () => ({
})),
}));
+jest.mock('./kibana_monitoring/collectors', () => ({
+ registerCollectors: jest.fn(),
+}));
+
describe('Monitoring plugin', () => {
const initializerContext = {
logger: {
@@ -70,6 +76,9 @@ describe('Monitoring plugin', () => {
subscribe: jest.fn(),
},
},
+ savedObjects: {
+ registerType: jest.fn(),
+ },
};
const setupPlugins = {
diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts
index c4b6d35d5f615..42bbeb4c74dbd 100644
--- a/x-pack/plugins/monitoring/server/plugin.ts
+++ b/x-pack/plugins/monitoring/server/plugin.ts
@@ -27,6 +27,7 @@ import {
KIBANA_MONITORING_LOGGING_TAG,
KIBANA_STATS_TYPE_MONITORING,
ALERTS,
+ SAVED_OBJECT_TELEMETRY,
} from '../common/constants';
import { MonitoringConfig, createConfig, configSchema } from './config';
// @ts-ignore
@@ -153,7 +154,20 @@ export class Plugin {
// Register collector objects for stats to show up in the APIs
if (plugins.usageCollection) {
- registerCollectors(plugins.usageCollection, config);
+ core.savedObjects.registerType({
+ name: SAVED_OBJECT_TELEMETRY,
+ hidden: true,
+ namespaceType: 'agnostic',
+ mappings: {
+ properties: {
+ reportedClusterUuids: {
+ type: 'keyword',
+ },
+ },
+ },
+ });
+
+ registerCollectors(plugins.usageCollection, config, cluster.callAsInternalUser);
}
// Always create the bulk uploader
diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
index 4dde78fb4cebb..a87ae3fb26159 100644
--- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
+++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
@@ -32,7 +32,10 @@ export type FetchData = (
export type HasData = () => Promise;
-export type ObservabilityFetchDataPlugins = Exclude;
+export type ObservabilityFetchDataPlugins = Exclude<
+ ObservabilityApp,
+ 'observability' | 'stack_monitoring'
+>;
export interface DataHandler<
T extends ObservabilityFetchDataPlugins = ObservabilityFetchDataPlugins
diff --git a/x-pack/plugins/observability/typings/common.ts b/x-pack/plugins/observability/typings/common.ts
index c1b01c847f164..845652031a578 100644
--- a/x-pack/plugins/observability/typings/common.ts
+++ b/x-pack/plugins/observability/typings/common.ts
@@ -4,7 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export type ObservabilityApp = 'infra_metrics' | 'infra_logs' | 'apm' | 'uptime' | 'observability';
+export type ObservabilityApp =
+ | 'infra_metrics'
+ | 'infra_logs'
+ | 'apm'
+ | 'uptime'
+ | 'observability'
+ | 'stack_monitoring';
export type PromiseReturnType = Func extends (...args: any[]) => Promise
? Value
diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
index 86b7889957c9f..1236f2ad9b559 100644
--- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
+++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
@@ -540,6 +540,91 @@
}
}
},
+ "monitoring": {
+ "properties": {
+ "hasMonitoringData": {
+ "type": "boolean"
+ },
+ "clusters": {
+ "properties": {
+ "license": {
+ "type": "keyword"
+ },
+ "clusterUuid": {
+ "type": "keyword"
+ },
+ "metricbeatUsed": {
+ "type": "boolean"
+ },
+ "elasticsearch": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "count": {
+ "type": "long"
+ },
+ "metricbeatUsed": {
+ "type": "boolean"
+ }
+ }
+ },
+ "kibana": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "count": {
+ "type": "long"
+ },
+ "metricbeatUsed": {
+ "type": "boolean"
+ }
+ }
+ },
+ "logstash": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "count": {
+ "type": "long"
+ },
+ "metricbeatUsed": {
+ "type": "boolean"
+ }
+ }
+ },
+ "beats": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "count": {
+ "type": "long"
+ },
+ "metricbeatUsed": {
+ "type": "boolean"
+ }
+ }
+ },
+ "apm": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "count": {
+ "type": "long"
+ },
+ "metricbeatUsed": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"rollups": {
"properties": {
"index_patterns": {