[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:
parent
912a67f06f
commit
0d54f07227
|
@ -53,7 +53,7 @@ pageLoadAssetSize:
|
|||
mapsLegacy: 116817
|
||||
mapsLegacyLicensing: 20214
|
||||
ml: 82187
|
||||
monitoring: 50000
|
||||
monitoring: 80000
|
||||
navigation: 37269
|
||||
newsfeed: 42228
|
||||
observability: 89709
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,6 +23,7 @@ export enum AlertMessageTokenType {
|
|||
}
|
||||
|
||||
export enum AlertParamType {
|
||||
TextField = 'textfield',
|
||||
Duration = 'duration',
|
||||
Percentage = 'percentage',
|
||||
Number = 'number',
|
||||
|
|
58
x-pack/plugins/monitoring/common/es_glob_patterns.ts
Normal file
58
x-pack/plugins/monitoring/common/es_glob_patterns.ts
Normal 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, '\\$&');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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} />
|
||||
|
||||
{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} />
|
||||
|
||||
{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()}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'> {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue