From ee7f1042face53d8e65a5b74ba8704c9f69d431b Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Fri, 25 Sep 2020 12:23:10 -0400 Subject: [PATCH] [Monitoring] Usage collection (#75878) (#78537) * First stab at some internal telemetry * Add missing files * mbCount telemetry * Include more data * Remove unused field * This file isn't used * Mock in tests * Add schema * Store schema * Use sample cluster instead * Fix telemetry schema * Fix type issues * Updates * Fix schema and tests * Add tests * Add tests * Go back to using an array * Fix schema * Add page view data * Remove debug * Handle loading scenario here * Add delay tracking too * Add clicks for setup mode * Add clicks for setup mode * Fix beats/apm page views * Fix typings # Conflicts: # x-pack/plugins/monitoring/common/constants.ts --- x-pack/plugins/monitoring/common/constants.ts | 7 + x-pack/plugins/monitoring/kibana.json | 3 +- .../monitoring/public/angular/index.ts | 2 + .../public/components/page_loading/index.js | 18 +- .../components/setup_mode/enter_button.tsx | 7 + .../plugins/monitoring/public/legacy_shims.ts | 5 +- .../monitoring/public/lib/setup_mode.tsx | 12 +- x-pack/plugins/monitoring/public/plugin.ts | 3 + x-pack/plugins/monitoring/public/types.ts | 2 + .../public/views/apm/instance/index.js | 22 +-- .../public/views/apm/instances/index.js | 54 +++--- .../public/views/apm/overview/index.js | 9 +- .../public/views/base_controller.js | 20 ++- .../public/views/beats/beat/index.js | 1 + .../public/views/beats/listing/index.js | 4 +- .../public/views/cluster/listing/index.js | 1 + .../public/views/cluster/overview/index.js | 1 + .../public/views/elasticsearch/ccr/index.js | 9 +- .../views/elasticsearch/ccr/shard/index.js | 11 +- .../elasticsearch/index/advanced/index.js | 1 + .../public/views/elasticsearch/index/index.js | 1 + .../views/elasticsearch/indices/index.js | 31 ++-- .../elasticsearch/node/advanced/index.js | 1 + .../public/views/elasticsearch/node/index.js | 7 + .../public/views/elasticsearch/nodes/index.js | 63 +++---- .../elasticsearch/overview/controller.js | 2 +- .../public/views/kibana/instance/index.js | 2 +- .../views/logstash/node/advanced/index.js | 1 + .../public/views/logstash/node/index.js | 1 + .../views/logstash/node/pipelines/index.js | 1 + .../collectors/get_usage_collector.test.ts | 161 ++++++++++++++++++ .../collectors/get_usage_collector.ts | 128 ++++++++++++++ .../kibana_monitoring/collectors/index.ts | 8 +- .../collectors/lib/fetch_es_usage.test.ts | 97 +++++++++++ .../collectors/lib/fetch_es_usage.ts | 122 +++++++++++++ .../collectors/lib/fetch_license_type.test.ts | 40 +++++ .../collectors/lib/fetch_license_type.ts | 57 +++++++ .../lib/fetch_stack_product_usage.test.ts | 117 +++++++++++++ .../lib/fetch_stack_product_usage.ts | 111 ++++++++++++ .../lib/get_stack_products_usage.test.ts | 31 ++++ .../lib/get_stack_products_usage.ts | 77 +++++++++ .../kibana_monitoring/collectors/types.ts | 27 +++ .../lib/alerts/get_ccs_index_pattern.ts | 3 + .../plugins/monitoring/server/plugin.test.ts | 11 +- x-pack/plugins/monitoring/server/plugin.ts | 16 +- .../typings/fetch_overview_data/index.ts | 5 +- .../plugins/observability/typings/common.ts | 8 +- .../schema/xpack_plugins.json | 85 +++++++++ 48 files changed, 1287 insertions(+), 119 deletions(-) create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.test.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.test.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.test.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.test.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_stack_product_usage.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.test.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/get_stack_products_usage.ts create mode 100644 x-pack/plugins/monitoring/server/kibana_monitoring/collectors/types.ts 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": {