[Monitoring][Alerting] Large shard alert (#89410)

* Shards alert draft

* Added index pattern validation

* Fixed ui/ux

* Optimizing the response

* CR feedback

* import fix

* Increased size limit
This commit is contained in:
igoristic 2021-02-03 09:19:40 -05:00 committed by GitHub
parent 912a67f06f
commit 0d54f07227
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 931 additions and 171 deletions

View file

@ -53,7 +53,7 @@ pageLoadAssetSize:
mapsLegacy: 116817
mapsLegacyLicensing: 20214
ml: 82187
monitoring: 50000
monitoring: 80000
navigation: 37269
newsfeed: 42228
observability: 89709

View file

@ -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,
];
/**

View file

@ -23,6 +23,7 @@ export enum AlertMessageTokenType {
}
export enum AlertParamType {
TextField = 'textfield',
Duration = 'duration',
Percentage = 'percentage',
Number = 'number',

View file

@ -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, '\\$&');
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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';

View file

@ -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> = (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> = (props) => {
case AlertParamType.Number:
return (
<AlertParamNumber
key={alertParamName}
name={alertParamName}
details={details}
value={value}
errors={errors[alertParamName]}
setAlertParams={setAlertParams}
/>
);
case AlertParamType.TextField:
return (
<AlertParamTextField
key={alertParamName}
name={alertParamName}
label={details?.label}

View file

@ -7,8 +7,8 @@ import React from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
import { ALERT_CPU_USAGE, ALERT_DETAILS } from '../../../common/constants';
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';
export function createCpuUsageAlertType(): AlertTypeModel<MonitoringAlertTypeParams> {
return {

View file

@ -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';

View file

@ -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: Props) => {
const { name, label, setAlertParams, errors } = props;
const { name, details, setAlertParams, errors } = props;
const [value, setValue] = useState(props.value);
return (
<EuiFormRow label={label} error={errors} isInvalid={errors?.length > 0}>
<EuiFormRow label={details.label as string} error={errors} isInvalid={errors?.length > 0}>
<EuiFieldNumber
compressed
value={value}
append={details.append as string}
onChange={(e) => {
let newValue = Number(e.target.value);
if (isNaN(newValue)) {

View file

@ -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: Props) => {
const { name, label, setAlertParams, errors, placeholder } = props;
const [value, setValue] = useState(props.value);
return (
<EuiFormRow label={label} error={errors} isInvalid={errors?.length > 0}>
<EuiFieldText
compressed
placeholder={placeholder}
value={value}
onChange={(e) => {
const newValue = e.target.value;
setValue(newValue);
setAlertParams(name, newValue);
}}
/>
</EuiFormRow>
);
};

View file

@ -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<ValidateOptions> {
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) => (
<Expression {...props} paramDetails={ALERT_DETAILS[ALERT_LARGE_SHARD_SIZE].paramDetails} />
),
validate,
defaultActionMessage: '{{context.internalFullMessage}}',
requiresAppContext: true,
};
}

View file

@ -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';

View file

@ -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';

View file

@ -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 = (
<EuiFlexItem grow={false}>
<AlertsBadge alerts={alertsList} />
</EuiFlexItem>
);
}
return (
<ClusterItemContainer {...props} url="elasticsearch" title="Elasticsearch">
<EuiFlexGrid columns={4}>
@ -433,29 +446,36 @@ export function ElasticsearchPanel(props) {
<EuiFlexItem>
<EuiPanel paddingSize="m">
<EuiTitle size="s">
<h3>
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeData}
href={goToIndices()}
data-test-subj="esNumberOfIndices"
aria-label={i18n.translate(
'xpack.monitoring.cluster.overview.esPanel.indicesCountLinkAriaLabel',
{
defaultMessage: 'Elasticsearch Indices: {indicesCount}',
values: { indicesCount: formatNumber(get(indices, 'count'), 'int_commas') },
}
)}
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.esPanel.indicesCountLinkLabel"
defaultMessage="Indices: {indicesCount}"
values={{ indicesCount: formatNumber(get(indices, 'count'), 'int_commas') }}
/>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h3>
<DisabledIfNoDataAndInSetupModeLink
setupModeEnabled={setupMode.enabled}
setupModeData={setupModeData}
href={goToIndices()}
data-test-subj="esNumberOfIndices"
aria-label={i18n.translate(
'xpack.monitoring.cluster.overview.esPanel.indicesCountLinkAriaLabel',
{
defaultMessage: 'Elasticsearch Indices: {indicesCount}',
values: {
indicesCount: formatNumber(get(indices, 'count'), 'int_commas'),
},
}
)}
>
<FormattedMessage
id="xpack.monitoring.cluster.overview.esPanel.indicesCountLinkLabel"
defaultMessage="Indices: {indicesCount}"
values={{ indicesCount: formatNumber(get(indices, 'count'), 'int_commas') }}
/>
</DisabledIfNoDataAndInSetupModeLink>
</h3>
</EuiTitle>
</EuiFlexItem>
{indicesAlertStatus}
</EuiFlexGroup>
<EuiHorizontalRule margin="m" />
<EuiDescriptionList type="column">
<EuiDescriptionListTitle className="eui-textBreakWord">

View file

@ -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 }) => {
</h1>
</EuiScreenReaderOnly>
<EuiPanel>
<IndexDetailStatus stats={indexSummary} />
<IndexDetailStatus stats={indexSummary} alerts={alerts} />
</EuiPanel>
<EuiSpacer size="m" />
<AlertsCallout alerts={alerts} />
<EuiSpacer size="m" />
<EuiPageContent>
<EuiFlexGrid columns={2} gutterSize="s">
{metricsToShow.map((metric, index) => (

View file

@ -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
<EuiPage>
<EuiPageBody>
<EuiPanel>
<IndexDetailStatus stats={indexSummary} />
<IndexDetailStatus stats={indexSummary} alerts={alerts} />
</EuiPanel>
<EuiSpacer size="m" />
<AlertsCallout alerts={alerts} />
<EuiSpacer size="m" />
<EuiPageContent>
<EuiFlexGrid columns={2} gutterSize="s">
{metricsToShow.map((metric, index) => (

View file

@ -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: <AlertsStatus alerts={alerts} showOnlyCount={true} />,
},
{
label: i18n.translate('xpack.monitoring.elasticsearch.indexDetailStatus.totalTitle', {
defaultMessage: 'Total',

View file

@ -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) => (
<div data-test-subj="name">
<EuiLink
href={getSafeForExternalLink(`#/elasticsearch/indices/${value}`)}
data-test-subj={`indexLink-${value}`}
>
{value}
</EuiLink>
</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.statusTitle', {
defaultMessage: 'Status',
}),
field: 'status',
sortable: true,
render: (value) => (
<div className="monElasticsearchIndicesTable__status" title={`Index status: ${value}`}>
<ElasticsearchStatusIcon status={value} />
&nbsp;
{capitalize(value)}
</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.documentCountTitle', {
defaultMessage: 'Document Count',
}),
field: 'doc_count',
sortable: true,
render: (value) => (
<div data-test-subj="documentCount">{formatMetric(value, LARGE_ABBREVIATED)}</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.dataTitle', {
defaultMessage: 'Data',
}),
field: 'data_size',
sortable: true,
render: (value) => <div data-test-subj="dataSize">{formatMetric(value, LARGE_BYTES)}</div>,
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.indexRateTitle', {
defaultMessage: 'Index Rate',
}),
field: 'index_rate',
sortable: true,
render: (value) => (
<div data-test-subj="indexRate">{formatMetric(value, LARGE_FLOAT, '/s')}</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.searchRateTitle', {
defaultMessage: 'Search Rate',
}),
field: 'search_rate',
sortable: true,
render: (value) => (
<div data-test-subj="searchRate">{formatMetric(value, LARGE_FLOAT, '/s')}</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.unassignedShardsTitle', {
defaultMessage: 'Unassigned Shards',
}),
field: 'unassigned_shards',
sortable: true,
render: (value) => <div data-test-subj="unassignedShards">{formatMetric(value, '0')}</div>,
},
];
const getColumns = (alerts) => {
return [
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.nameTitle', {
defaultMessage: 'Name',
}),
field: 'name',
width: '350px',
sortable: true,
render: (value) => (
<div data-test-subj="name">
<EuiLink
href={getSafeForExternalLink(`#/elasticsearch/indices/${value}`)}
data-test-subj={`indexLink-${value}`}
>
{value}
</EuiLink>
</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.alertsColumnTitle', {
defaultMessage: 'Alerts',
}),
field: 'alerts',
sortable: true,
render: (_field, index) => {
return (
<AlertsStatus
showBadge={true}
alerts={alerts}
stateFilter={(state) => state.meta.shardIndex === index.name}
/>
);
},
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.statusTitle', {
defaultMessage: 'Status',
}),
field: 'status',
sortable: true,
render: (value) => (
<div className="monElasticsearchIndicesTable__status" title={`Index status: ${value}`}>
<ElasticsearchStatusIcon status={value} />
&nbsp;
{capitalize(value)}
</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.documentCountTitle', {
defaultMessage: 'Document Count',
}),
field: 'doc_count',
sortable: true,
render: (value) => (
<div data-test-subj="documentCount">{formatMetric(value, LARGE_ABBREVIATED)}</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.dataTitle', {
defaultMessage: 'Data',
}),
field: 'data_size',
sortable: true,
render: (value) => <div data-test-subj="dataSize">{formatMetric(value, LARGE_BYTES)}</div>,
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.indexRateTitle', {
defaultMessage: 'Index Rate',
}),
field: 'index_rate',
sortable: true,
render: (value) => (
<div data-test-subj="indexRate">{formatMetric(value, LARGE_FLOAT, '/s')}</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.searchRateTitle', {
defaultMessage: 'Search Rate',
}),
field: 'search_rate',
sortable: true,
render: (value) => (
<div data-test-subj="searchRate">{formatMetric(value, LARGE_FLOAT, '/s')}</div>
),
},
{
name: i18n.translate('xpack.monitoring.elasticsearch.indices.unassignedShardsTitle', {
defaultMessage: 'Unassigned Shards',
}),
field: 'unassigned_shards',
sortable: true,
render: (value) => <div data-test-subj="unassignedShards">{formatMetric(value, '0')}</div>,
},
];
};
const getNoDataMessage = () => {
return (
@ -134,6 +153,7 @@ export const ElasticsearchIndices = ({
onTableChange,
toggleShowSystemIndices,
showSystemIndices,
alerts,
}) => {
return (
<EuiPage>
@ -147,7 +167,7 @@ export const ElasticsearchIndices = ({
</h1>
</EuiScreenReaderOnly>
<EuiPanel>
<ClusterStatus stats={clusterStatus} />
<ClusterStatus stats={clusterStatus} alerts={alerts} />
</EuiPanel>
<EuiSpacer size="m" />
<EuiPageContent>
@ -165,7 +185,7 @@ export const ElasticsearchIndices = ({
<EuiMonitoringTable
className="elasticsearchIndicesTable"
rows={indices}
columns={columns}
columns={getColumns(alerts)}
sorting={sorting}
pagination={pagination}
message={getNoDataMessage()}

View file

@ -136,7 +136,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler
<AlertsStatus
showBadge={true}
alerts={alerts}
stateFilter={(state) => state.nodeId === node.resolver}
stateFilter={(state) => (state.nodeId || state.nodeUuid) === node.resolver.uuid}
/>
);
},

View file

@ -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);

View file

@ -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(
<AdvancedIndex
indexSummary={data.indexSummary}
metrics={data.metrics}
onBrush={this.onBrush}
zoomInfo={this.zoomInfo}
<SetupModeRenderer
scope={$scope}
injector={$injector}
productName={ELASTICSEARCH_SYSTEM_ID}
render={({ setupMode, flyoutComponent, bottomBarComponent }) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>
{flyoutComponent}
<AdvancedIndex
scope={$scope}
setupMode={setupMode}
alerts={this.alerts}
indexSummary={data.indexSummary}
metrics={data.metrics}
onBrush={this.onBrush}
zoomInfo={this.zoomInfo}
/>
{bottomBarComponent}
</SetupModeContext.Provider>
)}
/>
);
}

View file

@ -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(
<Index
<SetupModeRenderer
scope={$scope}
onBrush={this.onBrush}
indexUuid={this.indexName}
clusterUuid={$scope.cluster.cluster_uuid}
zoomInfo={this.zoomInfo}
{...data}
injector={$injector}
productName={ELASTICSEARCH_SYSTEM_ID}
render={({ setupMode, flyoutComponent, bottomBarComponent }) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>
{flyoutComponent}
<Index
scope={$scope}
setupMode={setupMode}
alerts={this.alerts}
onBrush={this.onBrush}
indexUuid={this.indexName}
clusterUuid={$scope.cluster.cluster_uuid}
zoomInfo={this.zoomInfo}
{...data}
/>
{bottomBarComponent}
</SetupModeContext.Provider>
)}
/>
);
}

View file

@ -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(
<ElasticsearchIndices
clusterStatus={clusterStatus}
indices={indices}
showSystemIndices={showSystemIndices}
toggleShowSystemIndices={toggleShowSystemIndices}
sorting={this.sorting}
pagination={this.pagination}
onTableChange={this.onTableChange}
<SetupModeRenderer
scope={$scope}
injector={$injector}
productName={ELASTICSEARCH_SYSTEM_ID}
render={({ flyoutComponent, bottomBarComponent }) => (
<SetupModeContext.Provider value={{ setupModeSupported: true }}>
{flyoutComponent}
<ElasticsearchIndices
clusterStatus={clusterStatus}
indices={indices}
alerts={this.alerts}
showSystemIndices={showSystemIndices}
toggleShowSystemIndices={toggleShowSystemIndices}
sorting={this.sorting}
pagination={this.pagination}
onTableChange={this.onTableChange}
/>
{bottomBarComponent}
</SetupModeContext.Provider>
)}
/>
);
};

View file

@ -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 {

View file

@ -60,7 +60,7 @@ interface AlertOptions {
throttle?: string | null;
interval?: string;
legacy?: LegacyOptions;
defaultParams?: CommonAlertParams;
defaultParams?: Partial<CommonAlertParams>;
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<never, never, never, never, 'default'> {

View file

@ -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';

View file

@ -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<AlertData[]> {
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<CommonAlertFilter & { shardIndex: string }>
) {
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,
});
}
}

View file

@ -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<ElasticsearchIndexStats>; 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<IndexShardSizeStats[]> {
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;
}

View file

@ -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,
};
}