diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 74ee3f7c46e1..490c2ccc19d7 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -53,7 +53,7 @@ pageLoadAssetSize: mapsLegacy: 116817 mapsLegacyLicensing: 20214 ml: 82187 - monitoring: 50000 + monitoring: 80000 navigation: 37269 newsfeed: 42228 observability: 89709 diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index c8b3ae0be631..fda49c370293 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -252,6 +252,7 @@ export const ALERT_MISSING_MONITORING_DATA = `${ALERT_PREFIX}alert_missing_monit export const ALERT_THREAD_POOL_SEARCH_REJECTIONS = `${ALERT_PREFIX}alert_thread_pool_search_rejections`; export const ALERT_THREAD_POOL_WRITE_REJECTIONS = `${ALERT_PREFIX}alert_thread_pool_write_rejections`; export const ALERT_CCR_READ_EXCEPTIONS = `${ALERT_PREFIX}ccr_read_exceptions`; +export const ALERT_LARGE_SHARD_SIZE = `${ALERT_PREFIX}shard_size`; /** * Legacy alerts details/label for server and public use @@ -471,6 +472,30 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert if any CCR read exceptions have been detected.', }), }, + [ALERT_LARGE_SHARD_SIZE]: { + paramDetails: { + threshold: { + label: i18n.translate('xpack.monitoring.alerts.shardSize.paramDetails.threshold.label', { + defaultMessage: `Notify when a shard exceeds this size`, + }), + type: AlertParamType.Number, + append: 'GB', + }, + indexPattern: { + label: i18n.translate('xpack.monitoring.alerts.shardSize.paramDetails.indexPattern.label', { + defaultMessage: `Check the following index patterns`, + }), + placeholder: 'eg: data-*, *prod-data, -.internal-data*', + type: AlertParamType.TextField, + }, + }, + label: i18n.translate('xpack.monitoring.alerts.shardSize.label', { + defaultMessage: 'Shard size', + }), + description: i18n.translate('xpack.monitoring.alerts.shardSize.description', { + defaultMessage: 'Alert if an index (primary) shard is oversize.', + }), + }, }; export const ALERT_PANEL_MENU = [ @@ -494,6 +519,7 @@ export const ALERT_PANEL_MENU = [ { alertName: ALERT_CPU_USAGE }, { alertName: ALERT_DISK_USAGE }, { alertName: ALERT_MEMORY_USAGE }, + { alertName: ALERT_LARGE_SHARD_SIZE }, ], }, { @@ -527,6 +553,7 @@ export const ALERTS = [ ALERT_THREAD_POOL_SEARCH_REJECTIONS, ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_CCR_READ_EXCEPTIONS, + ALERT_LARGE_SHARD_SIZE, ]; /** diff --git a/x-pack/plugins/monitoring/common/enums.ts b/x-pack/plugins/monitoring/common/enums.ts index b373428bb279..e0a770a8b7e0 100644 --- a/x-pack/plugins/monitoring/common/enums.ts +++ b/x-pack/plugins/monitoring/common/enums.ts @@ -23,6 +23,7 @@ export enum AlertMessageTokenType { } export enum AlertParamType { + TextField = 'textfield', Duration = 'duration', Percentage = 'percentage', Number = 'number', diff --git a/x-pack/plugins/monitoring/common/es_glob_patterns.ts b/x-pack/plugins/monitoring/common/es_glob_patterns.ts new file mode 100644 index 000000000000..aa21622167ef --- /dev/null +++ b/x-pack/plugins/monitoring/common/es_glob_patterns.ts @@ -0,0 +1,58 @@ +/* + * 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 RegExPatterns { + contains?: string | RegExp; + negate?: string | RegExp; +} + +const valid = /.*/; + +export class ESGlobPatterns { + public static createRegExPatterns(globPattern: string) { + if (globPattern === '*') { + return { contains: valid, negate: valid }; + } + + globPattern = globPattern.toLowerCase(); + globPattern = globPattern.replace(/[ \\\/?"<>|#]/g, ''); + const patternsArr = globPattern.split(','); + const containPatterns: string[] = []; + const negatePatterns: string[] = []; + patternsArr.forEach((pattern) => { + if (pattern.charAt(0) === '-') { + negatePatterns.push(ESGlobPatterns.createESGlobRegExStr(pattern.slice(1))); + } else { + containPatterns.push(ESGlobPatterns.createESGlobRegExStr(pattern)); + } + }); + const contains = containPatterns.length ? new RegExp(containPatterns.join('|'), 'gi') : valid; + const negate = negatePatterns.length + ? new RegExp(`^((?!(${negatePatterns.join('|')})).)*$`, 'gi') + : valid; + return { contains, negate }; + } + + public static isValid(value: string, patterns: RegExPatterns) { + const { contains = valid, negate = valid } = patterns; + return new RegExp(contains).test(value) && new RegExp(negate).test(value); + } + + private static createESGlobRegExStr(pattern: string) { + const patternsArr = pattern.split('*'); + const firstItem = patternsArr.shift(); + const lastItem = patternsArr.pop(); + const start = firstItem?.length ? `(^${ESGlobPatterns.escapeStr(firstItem)})` : ''; + const mid = patternsArr.map((group) => `(.*${ESGlobPatterns.escapeStr(group)})`); + const end = lastItem?.length ? `(.*${ESGlobPatterns.escapeStr(lastItem)}$)` : ''; + const regExArr = ['(^', start, ...mid, end, ')']; + return regExArr.join(''); + } + + private static escapeStr(str: string) { + return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } +} diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index df6e169f37f7..cf358a4c5c9b 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -28,6 +28,7 @@ export interface CommonAlertFilter { export interface CommonAlertParamDetail { label: string; type?: AlertParamType; + [name: string]: unknown | undefined; } export interface CommonAlertParamDetails { @@ -38,6 +39,7 @@ export interface CommonAlertParams { duration: string; threshold?: number; limit?: string; + [key: string]: unknown; } export interface ThreadPoolRejectionsAlertParams { @@ -182,6 +184,18 @@ export interface CCRReadExceptionsUIMeta extends CCRReadExceptionsStats { itemLabel: string; } +export interface IndexShardSizeStats extends AlertNodeStats { + shardIndex: string; + shardSize: number; +} + +export interface IndexShardSizeUIMeta extends IndexShardSizeStats { + shardIndex: string; + shardSize: number; + instanceId: string; + itemLabel: string; +} + export interface AlertData { nodeName?: string; nodeId?: string; diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index 728cd3d73a34..613cde2f3213 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -97,6 +97,29 @@ export interface ElasticsearchNodeStats { }; } +export interface ElasticsearchIndexStats { + index?: string; + primaries?: { + docs?: { + count?: number; + }; + store?: { + size_in_bytes?: number; + }; + indexing?: { + index_total?: number; + }; + }; + total?: { + store?: { + size_in_bytes?: number; + }; + search?: { + query_total?: number; + }; + }; +} + export interface ElasticsearchLegacySource { timestamp: string; cluster_uuid: string; @@ -243,28 +266,7 @@ export interface ElasticsearchLegacySource { name?: string; }; }; - index_stats?: { - index?: string; - primaries?: { - docs?: { - count?: number; - }; - store?: { - size_in_bytes?: number; - }; - indexing?: { - index_total?: number; - }; - }; - total?: { - store?: { - size_in_bytes?: number; - }; - search?: { - query_total?: number; - }; - }; - }; + index_stats?: ElasticsearchIndexStats; node_stats?: ElasticsearchNodeStats; service?: { address?: string; diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx index e656c0ab253e..fab6cc35ad61 100644 --- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Expression, Props } from '../components/duration/expression'; +import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; import { ALERT_CCR_READ_EXCEPTIONS, ALERT_DETAILS } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerts/common'; diff --git a/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx similarity index 83% rename from x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx rename to x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx index 26593fdd6e7b..762f3ac5a3bf 100644 --- a/x-pack/plugins/monitoring/public/alerts/components/duration/expression.tsx +++ b/x-pack/plugins/monitoring/public/alerts/components/param_details_form/expression.tsx @@ -11,6 +11,7 @@ import { AlertParamDuration } from '../../flyout_expressions/alert_param_duratio import { AlertParamType } from '../../../../common/enums'; import { AlertParamPercentage } from '../../flyout_expressions/alert_param_percentage'; import { AlertParamNumber } from '../../flyout_expressions/alert_param_number'; +import { AlertParamTextField } from '../../flyout_expressions/alert_param_textfield'; export interface Props { alertParams: { [property: string]: any }; @@ -23,7 +24,7 @@ export interface Props { export const Expression: React.FC = (props) => { const { alertParams, paramDetails, setAlertParams, errors } = props; - const alertParamsUi = Object.keys(alertParams).map((alertParamName) => { + const alertParamsUi = Object.keys(paramDetails).map((alertParamName) => { const details = paramDetails[alertParamName]; const value = alertParams[alertParamName]; @@ -53,6 +54,17 @@ export const Expression: React.FC = (props) => { case AlertParamType.Number: return ( + ); + case AlertParamType.TextField: + return ( + { return { diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index aeb9bab2aae9..5d4db215eede 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { validate, MonitoringAlertTypeParams } from '../components/duration/validation'; -import { Expression, Props } from '../components/duration/expression'; +import { validate, MonitoringAlertTypeParams } from '../components/param_details_form/validation'; +import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_number.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_number.tsx index 82d44413806c..ce2c949b540e 100644 --- a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_number.tsx +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_number.tsx @@ -10,18 +10,19 @@ import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; interface Props { name: string; value: number; - label: string; + details: { [key: string]: unknown }; errors: string[]; setAlertParams: (property: string, value: number) => void; } export const AlertParamNumber: React.FC = (props: Props) => { - const { name, label, setAlertParams, errors } = props; + const { name, details, setAlertParams, errors } = props; const [value, setValue] = useState(props.value); return ( - 0}> + 0}> { let newValue = Number(e.target.value); if (isNaN(newValue)) { diff --git a/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_textfield.tsx b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_textfield.tsx new file mode 100644 index 000000000000..b30c15a37d7e --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/flyout_expressions/alert_param_textfield.tsx @@ -0,0 +1,35 @@ +/* + * 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 React, { useState } from 'react'; +import { EuiFormRow, EuiFieldText } from '@elastic/eui'; + +interface Props { + name: string; + value: string; + placeholder?: string; + label: string; + errors: string[]; + setAlertParams: (property: string, value: string) => void; +} +export const AlertParamTextField: React.FC = (props: Props) => { + const { name, label, setAlertParams, errors, placeholder } = props; + const [value, setValue] = useState(props.value); + return ( + 0}> + { + const newValue = e.target.value; + setValue(newValue); + setAlertParams(name, newValue); + }} + /> + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx new file mode 100644 index 000000000000..7bfa3cb151a2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx @@ -0,0 +1,49 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { Expression, Props } from '../components/param_details_form/expression'; +import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; +import { ALERT_LARGE_SHARD_SIZE, ALERT_DETAILS } from '../../../common/constants'; +import { AlertTypeParams } from '../../../../alerts/common'; + +interface ValidateOptions extends AlertTypeParams { + indexPattern: string; +} + +const validate = (inputValues: ValidateOptions): ValidationResult => { + const validationResult = { errors: {} }; + const errors: { [key: string]: string[] } = { + indexPattern: [], + }; + if (!inputValues.indexPattern) { + errors.indexPattern.push( + i18n.translate('xpack.monitoring.alerts.validation.indexPattern', { + defaultMessage: 'A valid index pattern/s is required.', + }) + ); + } + validationResult.errors = errors; + return validationResult; +}; + +export function createLargeShardSizeAlertType(): AlertTypeModel { + return { + id: ALERT_LARGE_SHARD_SIZE, + description: ALERT_DETAILS[ALERT_LARGE_SHARD_SIZE].description, + iconClass: 'bell', + documentationUrl(docLinks) { + return `${docLinks.links.monitoring.alertsKibana}`; + }, + alertParamsExpression: (props: Props) => ( + + ), + validate, + defaultActionMessage: '{{context.internalFullMessage}}', + requiresAppContext: true, + }; +} diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index b484cd9a975f..534e410d9edf 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { validate, MonitoringAlertTypeParams } from '../components/duration/validation'; -import { Expression, Props } from '../components/duration/expression'; +import { validate, MonitoringAlertTypeParams } from '../components/param_details_form/validation'; +import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx index 2c57857070d3..c5e0d3307936 100644 --- a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; -import { Expression, Props } from '../components/duration/expression'; +import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { CommonAlertParamDetails } from '../../../common/types/alerts'; diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 8849fb05fcf3..c19c8c5b8b06 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -48,6 +48,7 @@ import { ALERT_ELASTICSEARCH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA, ALERT_CCR_READ_EXCEPTIONS, + ALERT_LARGE_SHARD_SIZE, } from '../../../../common/constants'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; @@ -177,6 +178,8 @@ const NODES_PANEL_ALERTS = [ ALERT_MISSING_MONITORING_DATA, ]; +const INDICES_PANEL_ALERTS = [ALERT_LARGE_SHARD_SIZE]; + export function ElasticsearchPanel(props) { const clusterStats = props.cluster_stats || {}; const nodes = clusterStats.nodes; @@ -301,6 +304,16 @@ export function ElasticsearchPanel(props) { ); } + let indicesAlertStatus = null; + if (shouldShowAlertBadge(alerts, INDICES_PANEL_ALERTS, setupModeContext)) { + const alertsList = INDICES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + indicesAlertStatus = ( + + + + ); + } + return ( @@ -433,29 +446,36 @@ export function ElasticsearchPanel(props) { - -

- - - -

-
+ + + +

+ + + +

+
+
+ {indicesAlertStatus} +
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js index a0a339fabef9..78934bffb1a4 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js @@ -18,8 +18,9 @@ import { import { IndexDetailStatus } from '../index_detail_status'; import { MonitoringTimeseriesContainer } from '../../chart'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsCallout } from '../../../alerts/callout'; -export const AdvancedIndex = ({ indexSummary, metrics, ...props }) => { +export const AdvancedIndex = ({ indexSummary, metrics, alerts, ...props }) => { const metricsToShow = [ metrics.index_1, metrics.index_2, @@ -46,9 +47,11 @@ export const AdvancedIndex = ({ indexSummary, metrics, ...props }) => { - + + + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js index 9fdddd712e86..7a3069ad106e 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js @@ -18,8 +18,18 @@ import { IndexDetailStatus } from '../index_detail_status'; import { MonitoringTimeseriesContainer } from '../../chart'; import { ShardAllocation } from '../shard_allocation/shard_allocation'; import { Logs } from '../../logs'; +import { AlertsCallout } from '../../../alerts/callout'; -export const Index = ({ scope, indexSummary, metrics, clusterUuid, indexUuid, logs, ...props }) => { +export const Index = ({ + scope, + indexSummary, + metrics, + clusterUuid, + indexUuid, + logs, + alerts, + ...props +}) => { const metricsToShow = [ metrics.index_mem, metrics.index_size, @@ -33,9 +43,11 @@ export const Index = ({ scope, indexSummary, metrics, clusterUuid, indexUuid, lo - + + + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js index aece36d48a80..344a1dd64267 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js @@ -10,11 +10,18 @@ import { ElasticsearchStatusIcon } from '../status_icon'; import { formatMetric } from '../../../lib/format_number'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { AlertsStatus } from '../../../alerts/status'; -export function IndexDetailStatus({ stats }) { +export function IndexDetailStatus({ stats, alerts = {} }) { const { dataSize, documents: documentCount, totalShards, unassignedShards, status } = stats; const metrics = [ + { + label: i18n.translate('xpack.monitoring.elasticsearch.indexDetailStatus.alerts', { + defaultMessage: 'Alerts', + }), + value: , + }, { label: i18n.translate('xpack.monitoring.elasticsearch.indexDetailStatus.totalTitle', { defaultMessage: 'Total', diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js index a22487800c69..703f142c3a6f 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js @@ -24,88 +24,107 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsStatus } from '../../../alerts/status'; import './indices.scss'; -const columns = [ - { - name: i18n.translate('xpack.monitoring.elasticsearch.indices.nameTitle', { - defaultMessage: 'Name', - }), - field: 'name', - width: '350px', - sortable: true, - render: (value) => ( -
- - {value} - -
- ), - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.indices.statusTitle', { - defaultMessage: 'Status', - }), - field: 'status', - sortable: true, - render: (value) => ( -
- -   - {capitalize(value)} -
- ), - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.indices.documentCountTitle', { - defaultMessage: 'Document Count', - }), - field: 'doc_count', - sortable: true, - render: (value) => ( -
{formatMetric(value, LARGE_ABBREVIATED)}
- ), - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.indices.dataTitle', { - defaultMessage: 'Data', - }), - field: 'data_size', - sortable: true, - render: (value) =>
{formatMetric(value, LARGE_BYTES)}
, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.indices.indexRateTitle', { - defaultMessage: 'Index Rate', - }), - field: 'index_rate', - sortable: true, - render: (value) => ( -
{formatMetric(value, LARGE_FLOAT, '/s')}
- ), - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.indices.searchRateTitle', { - defaultMessage: 'Search Rate', - }), - field: 'search_rate', - sortable: true, - render: (value) => ( -
{formatMetric(value, LARGE_FLOAT, '/s')}
- ), - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.indices.unassignedShardsTitle', { - defaultMessage: 'Unassigned Shards', - }), - field: 'unassigned_shards', - sortable: true, - render: (value) =>
{formatMetric(value, '0')}
, - }, -]; +const getColumns = (alerts) => { + return [ + { + name: i18n.translate('xpack.monitoring.elasticsearch.indices.nameTitle', { + defaultMessage: 'Name', + }), + field: 'name', + width: '350px', + sortable: true, + render: (value) => ( +
+ + {value} + +
+ ), + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.indices.alertsColumnTitle', { + defaultMessage: 'Alerts', + }), + field: 'alerts', + sortable: true, + render: (_field, index) => { + return ( + state.meta.shardIndex === index.name} + /> + ); + }, + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.indices.statusTitle', { + defaultMessage: 'Status', + }), + field: 'status', + sortable: true, + render: (value) => ( +
+ +   + {capitalize(value)} +
+ ), + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.indices.documentCountTitle', { + defaultMessage: 'Document Count', + }), + field: 'doc_count', + sortable: true, + render: (value) => ( +
{formatMetric(value, LARGE_ABBREVIATED)}
+ ), + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.indices.dataTitle', { + defaultMessage: 'Data', + }), + field: 'data_size', + sortable: true, + render: (value) =>
{formatMetric(value, LARGE_BYTES)}
, + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.indices.indexRateTitle', { + defaultMessage: 'Index Rate', + }), + field: 'index_rate', + sortable: true, + render: (value) => ( +
{formatMetric(value, LARGE_FLOAT, '/s')}
+ ), + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.indices.searchRateTitle', { + defaultMessage: 'Search Rate', + }), + field: 'search_rate', + sortable: true, + render: (value) => ( +
{formatMetric(value, LARGE_FLOAT, '/s')}
+ ), + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.indices.unassignedShardsTitle', { + defaultMessage: 'Unassigned Shards', + }), + field: 'unassigned_shards', + sortable: true, + render: (value) =>
{formatMetric(value, '0')}
, + }, + ]; +}; const getNoDataMessage = () => { return ( @@ -134,6 +153,7 @@ export const ElasticsearchIndices = ({ onTableChange, toggleShowSystemIndices, showSystemIndices, + alerts, }) => { return ( @@ -147,7 +167,7 @@ export const ElasticsearchIndices = ({ - + @@ -165,7 +185,7 @@ export const ElasticsearchIndices = ({ state.nodeId === node.resolver} + stateFilter={(state) => (state.nodeId || state.nodeUuid) === node.resolver.uuid} /> ); }, diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 65c0c4d915f9..fc38de414661 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -34,6 +34,7 @@ import { createDiskUsageAlertType } from './alerts/disk_usage_alert'; import { createThreadPoolRejectionsAlertType } from './alerts/thread_pool_rejections_alert'; import { createMemoryUsageAlertType } from './alerts/memory_usage_alert'; import { createCCRReadExceptionsAlertType } from './alerts/ccr_read_exceptions_alert'; +import { createLargeShardSizeAlertType } from './alerts/large_shard_size_alert'; interface MonitoringSetupPluginDependencies { home?: HomePublicPluginSetup; @@ -164,6 +165,7 @@ export class MonitoringPlugin ) ); alertTypeRegistry.register(createCCRReadExceptionsAlertType()); + alertTypeRegistry.register(createLargeShardSizeAlertType()); const legacyAlertTypes = createLegacyAlertTypes(); for (const legacyAlertType of legacyAlertTypes) { alertTypeRegistry.register(legacyAlertType); 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 cfc36e360709..adc75f438098 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 @@ -16,7 +16,13 @@ import template from './index.html'; import { Legacy } from '../../../../legacy_shims'; import { AdvancedIndex } from '../../../../components/elasticsearch/index/advanced'; import { MonitoringViewBaseController } from '../../../base_controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; +import { + CODE_PATH_ELASTICSEARCH, + ALERT_LARGE_SHARD_SIZE, + ELASTICSEARCH_SYSTEM_ID, +} from '../../../../../common/constants'; +import { SetupModeContext } from '../../../../components/setup_mode/setup_mode_context'; +import { SetupModeRenderer } from '../../../../components/renderers'; function getPageData($injector) { const globalState = $injector.get('globalState'); @@ -70,6 +76,17 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', { reactNodeId: 'monitoringElasticsearchAdvancedIndexApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + filters: [ + { + shardIndex: $route.current.pathParams.index, + }, + ], + }, + }, }); this.indexName = indexName; @@ -78,11 +95,25 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', { () => this.data, (data) => { this.renderReact( - ( + + {flyoutComponent} + + {bottomBarComponent} + + )} /> ); } 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 76628a0a02e4..a4d516f917ab 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js @@ -18,7 +18,13 @@ import { labels } from '../../../components/elasticsearch/shard_allocation/lib/l import { indicesByNodes } from '../../../components/elasticsearch/shard_allocation/transformers/indices_by_nodes'; import { Index } from '../../../components/elasticsearch/index/index'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; +import { + CODE_PATH_ELASTICSEARCH, + ALERT_LARGE_SHARD_SIZE, + ELASTICSEARCH_SYSTEM_ID, +} from '../../../../common/constants'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; +import { SetupModeRenderer } from '../../../components/renderers'; function getPageData($injector) { const $http = $injector.get('$http'); @@ -78,6 +84,17 @@ uiRoutes.when('/elasticsearch/indices/:index', { reactNodeId: 'monitoringElasticsearchIndexApp', $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + filters: [ + { + shardIndex: $route.current.pathParams.index, + }, + ], + }, + }, }); this.indexName = indexName; @@ -101,13 +118,26 @@ uiRoutes.when('/elasticsearch/indices/:index', { } this.renderReact( - ( + + {flyoutComponent} + + {bottomBarComponent} + + )} /> ); } 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 3d32ffad2c3d..1936c80f4330 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js @@ -12,7 +12,13 @@ import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; import { ElasticsearchIndices } from '../../../components'; import template from './index.html'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; +import { + CODE_PATH_ELASTICSEARCH, + ELASTICSEARCH_SYSTEM_ID, + ALERT_LARGE_SHARD_SIZE, +} from '../../../../common/constants'; +import { SetupModeRenderer } from '../../../components/renderers'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; uiRoutes.when('/elasticsearch/indices', { template, @@ -50,6 +56,12 @@ uiRoutes.when('/elasticsearch/indices', { $injector, $scope, $injector, + alerts: { + shouldFetch: true, + options: { + alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + }, + }, }); this.isCcrEnabled = $scope.cluster.isCcrEnabled; @@ -67,14 +79,26 @@ uiRoutes.when('/elasticsearch/indices', { const renderComponent = () => { const { clusterStatus, indices } = this.data; this.renderReact( - ( + + {flyoutComponent} + + {bottomBarComponent} + + )} /> ); }; diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts index 64b7148d87d9..93d2b52c91a3 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -5,6 +5,7 @@ */ import { + LargeShardSizeAlert, CCRReadExceptionsAlert, CpuUsageAlert, MissingMonitoringDataAlert, @@ -34,6 +35,7 @@ import { ALERT_KIBANA_VERSION_MISMATCH, ALERT_ELASTICSEARCH_VERSION_MISMATCH, ALERT_CCR_READ_EXCEPTIONS, + ALERT_LARGE_SHARD_SIZE, } from '../../common/constants'; import { AlertsClient } from '../../../alerts/server'; import { Alert } from '../../../alerts/common'; @@ -52,6 +54,7 @@ const BY_TYPE = { [ALERT_KIBANA_VERSION_MISMATCH]: KibanaVersionMismatchAlert, [ALERT_ELASTICSEARCH_VERSION_MISMATCH]: ElasticsearchVersionMismatchAlert, [ALERT_CCR_READ_EXCEPTIONS]: CCRReadExceptionsAlert, + [ALERT_LARGE_SHARD_SIZE]: LargeShardSizeAlert, }; export class AlertsFactory { diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 2a5d956b04f7..7b5370e11bbf 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -60,7 +60,7 @@ interface AlertOptions { throttle?: string | null; interval?: string; legacy?: LegacyOptions; - defaultParams?: CommonAlertParams; + defaultParams?: Partial; actionVariables: Array<{ name: string; description: string }>; fetchClustersRange?: number; accessorKey?: string; @@ -89,8 +89,13 @@ export class BaseAlert { public rawAlert?: SanitizedAlert, public alertOptions: AlertOptions = defaultAlertOptions() ) { - this.alertOptions = { ...defaultAlertOptions(), ...this.alertOptions }; - this.scopedLogger = Globals.app.getLogger(alertOptions.id!); + const defaultOptions = defaultAlertOptions(); + defaultOptions.defaultParams = { + ...defaultOptions.defaultParams, + ...this.alertOptions.defaultParams, + }; + this.alertOptions = { ...defaultOptions, ...this.alertOptions }; + this.scopedLogger = Globals.app.getLogger(alertOptions.id); } public getAlertType(): AlertType { diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts index b58476a01dc1..79e95f3ff8cd 100644 --- a/x-pack/plugins/monitoring/server/alerts/index.ts +++ b/x-pack/plugins/monitoring/server/alerts/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { LargeShardSizeAlert } from './large_shard_size_alert'; export { CCRReadExceptionsAlert } from './ccr_read_exceptions_alert'; export { BaseAlert } from './base_alert'; export { CpuUsageAlert } from './cpu_usage_alert'; diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts new file mode 100644 index 000000000000..4389e23736b1 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts @@ -0,0 +1,231 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { BaseAlert } from './base_alert'; +import { + AlertData, + AlertCluster, + AlertState, + AlertMessage, + IndexShardSizeUIMeta, + AlertMessageTimeToken, + AlertMessageLinkToken, + AlertInstanceState, + CommonAlertParams, + CommonAlertFilter, + IndexShardSizeStats, +} from '../../common/types/alerts'; +import { AlertInstance } from '../../../alerts/server'; +import { + INDEX_PATTERN_ELASTICSEARCH, + ALERT_LARGE_SHARD_SIZE, + ALERT_DETAILS, +} from '../../common/constants'; +import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; +import { SanitizedAlert, RawAlertInstance } from '../../../alerts/common'; +import { AlertingDefaults, createLink } from './alert_helpers'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { Globals } from '../static_globals'; + +const MAX_INDICES_LIST = 10; +export class LargeShardSizeAlert extends BaseAlert { + constructor(public rawAlert?: SanitizedAlert) { + super(rawAlert, { + id: ALERT_LARGE_SHARD_SIZE, + name: ALERT_DETAILS[ALERT_LARGE_SHARD_SIZE].label, + throttle: '12h', + defaultParams: { indexPattern: '*', threshold: 55 }, + actionVariables: [ + { + name: 'shardIndices', + description: i18n.translate( + 'xpack.monitoring.alerts.shardSize.actionVariables.shardIndex', + { + defaultMessage: 'List of indices which are experiencing large shard size.', + } + ), + }, + ...Object.values(AlertingDefaults.ALERT_TYPE.context), + ], + }); + } + + protected async fetchData( + params: CommonAlertParams & { indexPattern: string }, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const { threshold, indexPattern: shardIndexPatterns } = params; + + const stats = await fetchIndexShardSize( + callCluster, + clusters, + esIndexPattern, + threshold!, + shardIndexPatterns, + Globals.app.config.ui.max_bucket_size + ); + + return stats.map((stat) => { + const { shardIndex, shardSize, clusterUuid, ccs } = stat; + return { + shouldFire: true, + severity: AlertSeverity.Danger, + meta: { + shardIndex, + shardSize, + instanceId: `${clusterUuid}:${shardIndex}`, + itemLabel: shardIndex, + }, + clusterUuid, + ccs, + }; + }); + } + + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { + const { shardIndex, shardSize } = item.meta as IndexShardSizeUIMeta; + return { + text: i18n.translate('xpack.monitoring.alerts.shardSize.ui.firingMessage', { + defaultMessage: `The following index: #start_link{shardIndex}#end_link has a large shard size of: {shardSize}GB at #absolute`, + values: { + shardIndex, + shardSize, + }, + }), + nextSteps: [ + createLink( + i18n.translate('xpack.monitoring.alerts.shardSize.ui.nextSteps.investigateIndex', { + defaultMessage: '#start_linkInvestigate detailed index stats#end_link', + }), + `elasticsearch/indices/${shardIndex}/advanced`, + AlertMessageTokenType.Link + ), + createLink( + i18n.translate('xpack.monitoring.alerts.shardSize.ui.nextSteps.sizeYourShards', { + defaultMessage: '#start_linkHow to size your shards (Docs)#end_link', + }), + `{elasticWebsiteUrl}guide/en/elasticsearch/reference/current/size-your-shards.html` + ), + createLink( + i18n.translate('xpack.monitoring.alerts.shardSize.ui.nextSteps.shardSizingBlog', { + defaultMessage: '#start_linkShard sizing tips (Blog)#end_link', + }), + `{elasticWebsiteUrl}blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster` + ), + ], + tokens: [ + { + startToken: '#absolute', + type: AlertMessageTokenType.Time, + isAbsolute: true, + isRelative: false, + timestamp: alertState.ui.triggeredMS, + } as AlertMessageTimeToken, + { + startToken: '#start_link', + endToken: '#end_link', + type: AlertMessageTokenType.Link, + url: `elasticsearch/indices/${shardIndex}`, + } as AlertMessageLinkToken, + ], + }; + } + + protected filterAlertInstance( + alertInstance: RawAlertInstance, + filters: Array + ) { + const alertInstanceStates = alertInstance.state?.alertStates as AlertState[]; + const alertFilter = filters?.find((filter) => filter.shardIndex); + if (!filters || !filters.length || !alertInstanceStates?.length || !alertFilter?.shardIndex) { + return alertInstance; + } + const alertStates = alertInstanceStates.filter( + ({ meta }) => (meta as IndexShardSizeStats).shardIndex === alertFilter.shardIndex + ); + return { state: { alertStates } }; + } + + protected executeActions( + instance: AlertInstance, + { alertStates }: AlertInstanceState, + item: AlertData | null, + cluster: AlertCluster + ) { + let sortedAlertStates = alertStates.slice(0).sort((alertStateA, alertStateB) => { + const { meta: metaA } = alertStateA as { meta?: IndexShardSizeUIMeta }; + const { meta: metaB } = alertStateB as { meta?: IndexShardSizeUIMeta }; + return metaB!.shardSize - metaA!.shardSize; + }); + + let suffix = ''; + if (sortedAlertStates.length > MAX_INDICES_LIST) { + const diff = sortedAlertStates.length - MAX_INDICES_LIST; + sortedAlertStates = sortedAlertStates.slice(0, MAX_INDICES_LIST); + suffix = `, and ${diff} more`; + } + + const shardIndices = + sortedAlertStates + .map((alertState) => (alertState.meta as IndexShardSizeUIMeta).shardIndex) + .join(', ') + suffix; + + const shortActionText = i18n.translate('xpack.monitoring.alerts.shardSize.shortAction', { + defaultMessage: 'Investigate indices with large shard sizes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.shardSize.fullAction', { + defaultMessage: 'View index shard size stats', + }); + + const ccs = alertStates.find((state) => state.ccs)?.ccs; + const globalStateLink = this.createGlobalStateLink( + 'elasticsearch/indices', + cluster.clusterUuid, + ccs + ); + + const action = `[${fullActionText}](${globalStateLink})`; + const internalShortMessage = i18n.translate( + 'xpack.monitoring.alerts.shardSize.firing.internalShortMessage', + { + defaultMessage: `Large shard size alert is firing for the following indices: {shardIndices}. {shortActionText}`, + values: { + shardIndices, + shortActionText, + }, + } + ); + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.shardSize.firing.internalFullMessage', + { + defaultMessage: `Large shard size alert is firing for the following indices: {shardIndices}. {action}`, + values: { + action, + shardIndices, + }, + } + ); + + instance.scheduleActions('default', { + internalShortMessage, + internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + shardIndices, + clusterName: cluster.clusterName, + action, + actionPlain: shortActionText, + }); + } +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts new file mode 100644 index 000000000000..fd653657f811 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_index_shard_size.ts @@ -0,0 +1,157 @@ +/* + * 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 { AlertCluster, IndexShardSizeStats } from '../../../common/types/alerts'; +import { ElasticsearchIndexStats, ElasticsearchResponseHit } from '../../../common/types/es'; +import { ESGlobPatterns, RegExPatterns } from '../../../common/es_glob_patterns'; +import { Globals } from '../../static_globals'; + +interface SourceNode { + name: string; + uuid: string; +} +type TopHitType = ElasticsearchResponseHit & { + _source: { index_stats: Partial; source_node: SourceNode }; +}; + +const memoizedIndexPatterns = (globPatterns: string) => { + const createRegExPatterns = () => ESGlobPatterns.createRegExPatterns(globPatterns); + return Globals.app.getKeyStoreValue( + `large_shard_size_alert::${globPatterns}`, + createRegExPatterns + ) as RegExPatterns; +}; + +const gbMultiplier = 1000000000; + +export async function fetchIndexShardSize( + callCluster: any, + clusters: AlertCluster[], + index: string, + threshold: number, + shardIndexPatterns: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations.clusters.buckets'], + body: { + size: 0, + query: { + bool: { + must: [ + { + match: { + type: 'index_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-5m', + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + include: clusters.map((cluster) => cluster.clusterUuid), + field: 'cluster_uuid', + size, + }, + aggs: { + over_threshold: { + filter: { + range: { + 'index_stats.primaries.store.size_in_bytes': { + gt: threshold * gbMultiplier, + }, + }, + }, + aggs: { + index: { + terms: { + field: 'index_stats.index', + size, + }, + aggs: { + hits: { + top_hits: { + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + _source: { + includes: [ + '_index', + 'index_stats.primaries.store.size_in_bytes', + 'source_node.name', + 'source_node.uuid', + ], + }, + size: 1, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const stats: IndexShardSizeStats[] = []; + const { buckets: clusterBuckets = [] } = response.aggregations.clusters; + const validIndexPatterns = memoizedIndexPatterns(shardIndexPatterns); + + if (!clusterBuckets.length) { + return stats; + } + + for (const clusterBucket of clusterBuckets) { + const indexBuckets = clusterBucket.over_threshold.index.buckets; + const clusterUuid = clusterBucket.key; + + for (const indexBucket of indexBuckets) { + const shardIndex = indexBucket.key; + const topHit = indexBucket.hits?.hits?.hits[0] as TopHitType; + if ( + !topHit || + shardIndex.charAt() === '.' || + !ESGlobPatterns.isValid(shardIndex, validIndexPatterns) + ) { + continue; + } + const { + _index: monitoringIndexName, + _source: { source_node: sourceNode, index_stats: indexStats }, + } = topHit; + + const { size_in_bytes: shardSizeBytes } = indexStats?.primaries?.store!; + const { name: nodeName, uuid: nodeId } = sourceNode; + const shardSize = +(shardSizeBytes! / gbMultiplier).toFixed(2); + stats.push({ + shardIndex, + shardSize, + clusterUuid, + nodeName, + nodeId, + ccs: monitoringIndexName.includes(':') ? monitoringIndexName.split(':')[0] : undefined, + }); + } + } + return stats; +} diff --git a/x-pack/plugins/monitoring/server/static_globals.ts b/x-pack/plugins/monitoring/server/static_globals.ts index afa26f25919f..19d3263b66cd 100644 --- a/x-pack/plugins/monitoring/server/static_globals.ts +++ b/x-pack/plugins/monitoring/server/static_globals.ts @@ -17,8 +17,22 @@ interface IAppGlobals { monitoringCluster: ILegacyCustomClusterClient; config: MonitoringConfig; getLogger: GetLogger; + getKeyStoreValue: (key: string, storeValueMethod?: () => unknown) => unknown; } +interface KeyStoreData { + [key: string]: unknown; +} + +const keyStoreData: KeyStoreData = {}; +const getKeyStoreValue = (key: string, storeValueMethod?: () => unknown) => { + const value = keyStoreData[key]; + if ((value === undefined || value == null) && typeof storeValueMethod === 'function') { + keyStoreData[key] = storeValueMethod(); + } + return keyStoreData[key]; +}; + export class Globals { private static _app: IAppGlobals; @@ -37,6 +51,7 @@ export class Globals { monitoringCluster, config, getLogger, + getKeyStoreValue, }; }