[Monitoring] Missing data alert (#78208)
* WIP for alert * Surface alert most places * Fix up alert placement * Fix tests * Type fix * Update copy * Add alert presence to APM in the UI * Fetch data a little differently * We don't need moment * Add tests * PR feedback * Update copy * Fix up bug around grabbing old data * PR feedback * PR feedback * Fix tests
This commit is contained in:
parent
198c5d9988
commit
a61f4d4cbf
|
@ -236,6 +236,7 @@ export const ALERT_NODES_CHANGED = `${ALERT_PREFIX}alert_nodes_changed`;
|
|||
export const ALERT_ELASTICSEARCH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_elasticsearch_version_mismatch`;
|
||||
export const ALERT_KIBANA_VERSION_MISMATCH = `${ALERT_PREFIX}alert_kibana_version_mismatch`;
|
||||
export const ALERT_LOGSTASH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_logstash_version_mismatch`;
|
||||
export const ALERT_MISSING_MONITORING_DATA = `${ALERT_PREFIX}alert_missing_monitoring_data`;
|
||||
|
||||
/**
|
||||
* A listing of all alert types
|
||||
|
@ -249,6 +250,7 @@ export const ALERTS = [
|
|||
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
|
||||
ALERT_KIBANA_VERSION_MISMATCH,
|
||||
ALERT_LOGSTASH_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,10 +31,14 @@ export interface CommonAlertFilter {
|
|||
nodeUuid?: string;
|
||||
}
|
||||
|
||||
export interface CommonAlertCpuUsageFilter extends CommonAlertFilter {
|
||||
export interface CommonAlertNodeUuidFilter extends CommonAlertFilter {
|
||||
nodeUuid: string;
|
||||
}
|
||||
|
||||
export interface CommonAlertStackProductFilter extends CommonAlertFilter {
|
||||
stackProduct: string;
|
||||
}
|
||||
|
||||
export interface CommonAlertParamDetail {
|
||||
label: string;
|
||||
type: AlertParamType;
|
||||
|
|
|
@ -18,7 +18,7 @@ import { CommonAlertStatus, CommonAlertState } from '../../common/types';
|
|||
import { AlertSeverity } from '../../common/enums';
|
||||
// @ts-ignore
|
||||
import { formatDateTimeLocal } from '../../common/formatting';
|
||||
import { AlertState } from '../../server/alerts/types';
|
||||
import { AlertMessage, AlertState } from '../../server/alerts/types';
|
||||
import { AlertPanel } from './panel';
|
||||
import { Legacy } from '../legacy_shims';
|
||||
import { isInSetupMode } from '../lib/setup_mode';
|
||||
|
@ -39,9 +39,10 @@ interface AlertInPanel {
|
|||
interface Props {
|
||||
alerts: { [alertTypeId: string]: CommonAlertStatus };
|
||||
stateFilter: (state: AlertState) => boolean;
|
||||
nextStepsFilter: (nextStep: AlertMessage) => boolean;
|
||||
}
|
||||
export const AlertsBadge: React.FC<Props> = (props: Props) => {
|
||||
const { stateFilter = () => true } = props;
|
||||
const { stateFilter = () => true, nextStepsFilter = () => true } = props;
|
||||
const [showPopover, setShowPopover] = React.useState<AlertSeverity | boolean | null>(null);
|
||||
const inSetupMode = isInSetupMode();
|
||||
const alerts = Object.values(props.alerts).filter(Boolean);
|
||||
|
@ -80,7 +81,7 @@ export const AlertsBadge: React.FC<Props> = (props: Props) => {
|
|||
id: index + 1,
|
||||
title: alertStatus.alert.label,
|
||||
width: 400,
|
||||
content: <AlertPanel alert={alertStatus} />,
|
||||
content: <AlertPanel alert={alertStatus} nextStepsFilter={nextStepsFilter} />,
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
@ -158,7 +159,13 @@ export const AlertsBadge: React.FC<Props> = (props: Props) => {
|
|||
id: index + 1,
|
||||
title: getDateFromState(alertStatus.alertState),
|
||||
width: 400,
|
||||
content: <AlertPanel alert={alertStatus.alert} alertState={alertStatus.alertState} />,
|
||||
content: (
|
||||
<AlertPanel
|
||||
alert={alertStatus.alert}
|
||||
alertState={alertStatus.alertState}
|
||||
nextStepsFilter={nextStepsFilter}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
|
|
@ -32,9 +32,10 @@ const TYPES = [
|
|||
interface Props {
|
||||
alerts: { [alertTypeId: string]: CommonAlertStatus };
|
||||
stateFilter: (state: AlertState) => boolean;
|
||||
nextStepsFilter: (nextStep: AlertMessage) => boolean;
|
||||
}
|
||||
export const AlertsCallout: React.FC<Props> = (props: Props) => {
|
||||
const { alerts, stateFilter = () => true } = props;
|
||||
const { alerts, stateFilter = () => true, nextStepsFilter = () => true } = props;
|
||||
|
||||
const callouts = TYPES.map((type) => {
|
||||
const list = [];
|
||||
|
@ -56,11 +57,11 @@ export const AlertsCallout: React.FC<Props> = (props: Props) => {
|
|||
const nextStepsUi =
|
||||
state.ui.message.nextSteps && state.ui.message.nextSteps.length ? (
|
||||
<ul>
|
||||
{state.ui.message.nextSteps.map(
|
||||
(step: AlertMessage, nextStepIndex: number) => (
|
||||
{state.ui.message.nextSteps
|
||||
.filter(nextStepsFilter)
|
||||
.map((step: AlertMessage, nextStepIndex: number) => (
|
||||
<li key={nextStepIndex}>{replaceTokens(step)}</li>
|
||||
)
|
||||
)}
|
||||
))}
|
||||
</ul>
|
||||
) : null;
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { CommonAlertState, CommonAlertStatus } from '../../common/types';
|
||||
|
||||
export function filterAlertStates(
|
||||
alerts: { [type: string]: CommonAlertStatus },
|
||||
filter: (type: string, state: CommonAlertState) => boolean
|
||||
) {
|
||||
return Object.keys(alerts).reduce(
|
||||
(accum: { [type: string]: CommonAlertStatus }, type: string) => {
|
||||
accum[type] = {
|
||||
...alerts[type],
|
||||
states: alerts[type].states.filter((state) => filter(type, state)),
|
||||
};
|
||||
return accum;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
import { EuiForm, EuiSpacer } from '@elastic/eui';
|
||||
import { CommonAlertParamDetails } from '../../../common/types';
|
||||
import { AlertParamDuration } from '../flyout_expressions/alert_param_duration';
|
||||
import { AlertParamType } from '../../../common/enums';
|
||||
import { AlertParamPercentage } from '../flyout_expressions/alert_param_percentage';
|
||||
|
||||
export interface Props {
|
||||
alertParams: { [property: string]: any };
|
||||
setAlertParams: (property: string, value: any) => void;
|
||||
setAlertProperty: (property: string, value: any) => void;
|
||||
errors: { [key: string]: string[] };
|
||||
paramDetails: CommonAlertParamDetails;
|
||||
}
|
||||
|
||||
export const Expression: React.FC<Props> = (props) => {
|
||||
const { alertParams, paramDetails, setAlertParams, errors } = props;
|
||||
|
||||
const alertParamsUi = Object.keys(alertParams).map((alertParamName) => {
|
||||
const details = paramDetails[alertParamName];
|
||||
const value = alertParams[alertParamName];
|
||||
|
||||
switch (details.type) {
|
||||
case AlertParamType.Duration:
|
||||
return (
|
||||
<AlertParamDuration
|
||||
key={alertParamName}
|
||||
name={alertParamName}
|
||||
duration={value}
|
||||
label={details.label}
|
||||
errors={errors[alertParamName]}
|
||||
setAlertParams={setAlertParams}
|
||||
/>
|
||||
);
|
||||
case AlertParamType.Percentage:
|
||||
return (
|
||||
<AlertParamPercentage
|
||||
key={alertParamName}
|
||||
name={alertParamName}
|
||||
label={details.label}
|
||||
percentage={value}
|
||||
errors={errors[alertParamName]}
|
||||
setAlertParams={setAlertParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiForm component="form">{alertParamsUi}</EuiForm>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { createMissingMonitoringDataAlertType } from './missing_monitoring_data_alert';
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
|
||||
import { validate } from './validation';
|
||||
import { ALERT_MISSING_MONITORING_DATA } from '../../../common/constants';
|
||||
import { Expression } from './expression';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { MissingMonitoringDataAlert } from '../../../server/alerts';
|
||||
|
||||
export function createMissingMonitoringDataAlertType(): AlertTypeModel {
|
||||
const alert = new MissingMonitoringDataAlert();
|
||||
return {
|
||||
id: ALERT_MISSING_MONITORING_DATA,
|
||||
name: alert.label,
|
||||
iconClass: 'bell',
|
||||
alertParamsExpression: (props: any) => (
|
||||
<Expression {...props} paramDetails={MissingMonitoringDataAlert.paramDetails} />
|
||||
),
|
||||
validate,
|
||||
defaultActionMessage: '{{context.internalFullMessage}}',
|
||||
requiresAppContext: true,
|
||||
};
|
||||
}
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { ValidationResult } from '../../../../triggers_actions_ui/public/types';
|
||||
|
||||
export function validate(opts: any): ValidationResult {
|
||||
const validationResult = { errors: {} };
|
||||
|
||||
const errors: { [key: string]: string[] } = {
|
||||
duration: [],
|
||||
limit: [],
|
||||
};
|
||||
if (!opts.duration) {
|
||||
errors.duration.push(
|
||||
i18n.translate('xpack.monitoring.alerts.missingData.validation.duration', {
|
||||
defaultMessage: 'A valid duration is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!opts.limit) {
|
||||
errors.limit.push(
|
||||
i18n.translate('xpack.monitoring.alerts.missingData.validation.limit', {
|
||||
defaultMessage: 'A valid limit is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
validationResult.errors = errors;
|
||||
return validationResult;
|
||||
}
|
|
@ -30,11 +30,13 @@ import { BASE_ALERT_API_PATH } from '../../../alerts/common';
|
|||
interface Props {
|
||||
alert: CommonAlertStatus;
|
||||
alertState?: CommonAlertState;
|
||||
nextStepsFilter: (nextStep: AlertMessage) => boolean;
|
||||
}
|
||||
export const AlertPanel: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
alert: { alert },
|
||||
alertState,
|
||||
nextStepsFilter = () => true,
|
||||
} = props;
|
||||
const [showFlyout, setShowFlyout] = React.useState(false);
|
||||
const [isEnabled, setIsEnabled] = React.useState(alert.rawAlert.enabled);
|
||||
|
@ -198,9 +200,11 @@ export const AlertPanel: React.FC<Props> = (props: Props) => {
|
|||
const nextStepsUi =
|
||||
alertState.state.ui.message.nextSteps && alertState.state.ui.message.nextSteps.length ? (
|
||||
<EuiListGroup>
|
||||
{alertState.state.ui.message.nextSteps.map((step: AlertMessage, index: number) => (
|
||||
<EuiListGroupItem size="s" key={index} label={replaceTokens(step)} />
|
||||
))}
|
||||
{alertState.state.ui.message.nextSteps
|
||||
.filter(nextStepsFilter)
|
||||
.map((step: AlertMessage, index: number) => (
|
||||
<EuiListGroupItem size="s" key={index} label={replaceTokens(step)} />
|
||||
))}
|
||||
</EuiListGroup>
|
||||
) : null;
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { CommonAlertStatus } from '../../common/types';
|
||||
import { AlertSeverity } from '../../common/enums';
|
||||
import { AlertState } from '../../server/alerts/types';
|
||||
import { AlertMessage, AlertState } from '../../server/alerts/types';
|
||||
import { AlertsBadge } from './badge';
|
||||
import { isInSetupMode } from '../lib/setup_mode';
|
||||
|
||||
|
@ -18,9 +18,16 @@ interface Props {
|
|||
showBadge: boolean;
|
||||
showOnlyCount: boolean;
|
||||
stateFilter: (state: AlertState) => boolean;
|
||||
nextStepsFilter: (nextStep: AlertMessage) => boolean;
|
||||
}
|
||||
export const AlertsStatus: React.FC<Props> = (props: Props) => {
|
||||
const { alerts, showBadge = false, showOnlyCount = false, stateFilter = () => true } = props;
|
||||
const {
|
||||
alerts,
|
||||
showBadge = false,
|
||||
showOnlyCount = false,
|
||||
stateFilter = () => true,
|
||||
nextStepsFilter = () => true,
|
||||
} = props;
|
||||
const inSetupMode = isInSetupMode();
|
||||
|
||||
if (!alerts) {
|
||||
|
@ -71,7 +78,9 @@ export const AlertsStatus: React.FC<Props> = (props: Props) => {
|
|||
}
|
||||
|
||||
if (showBadge || inSetupMode) {
|
||||
return <AlertsBadge alerts={alerts} stateFilter={stateFilter} />;
|
||||
return (
|
||||
<AlertsBadge alerts={alerts} stateFilter={stateFilter} nextStepsFilter={nextStepsFilter} />
|
||||
);
|
||||
}
|
||||
|
||||
const severity = atLeastOneDanger ? AlertSeverity.Danger : AlertSeverity.Warning;
|
||||
|
|
|
@ -18,8 +18,9 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { Status } from './status';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { AlertsCallout } from '../../../alerts/callout';
|
||||
|
||||
export function ApmServerInstance({ summary, metrics, ...props }) {
|
||||
export function ApmServerInstance({ summary, metrics, alerts, ...props }) {
|
||||
const seriesToShow = [
|
||||
metrics.apm_requests,
|
||||
metrics.apm_responses_valid,
|
||||
|
@ -58,9 +59,18 @@ export function ApmServerInstance({ summary, metrics, ...props }) {
|
|||
</h1>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiPanel>
|
||||
<Status stats={summary} />
|
||||
<Status stats={summary} alerts={alerts} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<AlertsCallout
|
||||
alerts={alerts}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('APM servers')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<EuiPageContent>
|
||||
<EuiFlexGroup wrap>{charts}</EuiFlexGroup>
|
||||
</EuiPageContent>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { CALCULATE_DURATION_SINCE } from '../../../../common/constants';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function Status({ stats }) {
|
||||
export function Status({ alerts, stats }) {
|
||||
const { name, output, version, uptime, timeOfLastEvent } = stats;
|
||||
|
||||
const metrics = [
|
||||
|
@ -78,6 +78,7 @@ export function Status({ stats }) {
|
|||
return (
|
||||
<SummaryStatus
|
||||
metrics={metrics}
|
||||
alerts={alerts}
|
||||
IconComponent={IconComponent}
|
||||
data-test-subj="apmDetailStatus"
|
||||
/>
|
||||
|
|
|
@ -28,8 +28,9 @@ import { SetupModeBadge } from '../../setup_mode/badge';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
|
||||
import { SetupModeFeature } from '../../../../common/enums';
|
||||
import { AlertsStatus } from '../../../alerts/status';
|
||||
|
||||
function getColumns(setupMode) {
|
||||
function getColumns(alerts, setupMode) {
|
||||
return [
|
||||
{
|
||||
name: i18n.translate('xpack.monitoring.apm.instances.nameTitle', {
|
||||
|
@ -71,6 +72,29 @@ function getColumns(setupMode) {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.monitoring.beats.instances.alertsColumnTitle', {
|
||||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
field: 'alerts',
|
||||
width: '175px',
|
||||
sortable: true,
|
||||
render: (_field, beat) => {
|
||||
return (
|
||||
<AlertsStatus
|
||||
showBadge={true}
|
||||
alerts={alerts}
|
||||
stateFilter={(state) => state.stackProductUuid === beat.uuid}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('APM servers')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.monitoring.apm.instances.outputEnabledTitle', {
|
||||
defaultMessage: 'Output Enabled',
|
||||
|
@ -127,7 +151,7 @@ function getColumns(setupMode) {
|
|||
];
|
||||
}
|
||||
|
||||
export function ApmServerInstances({ apms, setupMode }) {
|
||||
export function ApmServerInstances({ apms, alerts, setupMode }) {
|
||||
const { pagination, sorting, onTableChange, data } = apms;
|
||||
|
||||
let setupModeCallout = null;
|
||||
|
@ -157,7 +181,7 @@ export function ApmServerInstances({ apms, setupMode }) {
|
|||
</h1>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiPanel>
|
||||
<Status stats={data.stats} />
|
||||
<Status stats={data.stats} alerts={alerts} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPageContent>
|
||||
|
@ -165,7 +189,7 @@ export function ApmServerInstances({ apms, setupMode }) {
|
|||
<EuiMonitoringTable
|
||||
className="apmInstancesTable"
|
||||
rows={data.apms}
|
||||
columns={getColumns(setupMode)}
|
||||
columns={getColumns(alerts, setupMode)}
|
||||
sorting={sorting}
|
||||
pagination={pagination}
|
||||
setupMode={setupMode}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { CALCULATE_DURATION_SINCE } from '../../../../common/constants';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function Status({ stats }) {
|
||||
export function Status({ alerts, stats }) {
|
||||
const {
|
||||
apms: { total },
|
||||
totalEvents,
|
||||
|
@ -68,6 +68,7 @@ export function Status({ stats }) {
|
|||
return (
|
||||
<SummaryStatus
|
||||
metrics={metrics}
|
||||
alerts={alerts}
|
||||
IconComponent={IconComponent}
|
||||
data-test-subj="apmDetailStatus"
|
||||
/>
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { Status } from '../instances/status';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
export function ApmOverview({ stats, metrics, ...props }) {
|
||||
export function ApmOverview({ stats, metrics, alerts, ...props }) {
|
||||
const seriesToShow = [
|
||||
metrics.apm_responses_valid,
|
||||
metrics.apm_responses_errors,
|
||||
|
@ -54,7 +54,7 @@ export function ApmOverview({ stats, metrics, ...props }) {
|
|||
</h1>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiPanel>
|
||||
<Status stats={stats} />
|
||||
<Status stats={stats} alerts={alerts} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPageContent>
|
||||
|
|
|
@ -20,8 +20,9 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { SummaryStatus } from '../../summary_status';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { AlertsCallout } from '../../../alerts/callout';
|
||||
|
||||
export function Beat({ summary, metrics, ...props }) {
|
||||
export function Beat({ summary, metrics, alerts, ...props }) {
|
||||
const metricsToShow = [
|
||||
metrics.beat_event_rates,
|
||||
metrics.beat_fail_rates,
|
||||
|
@ -134,13 +135,26 @@ export function Beat({ summary, metrics, ...props }) {
|
|||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPanel>
|
||||
<SummaryStatus metrics={summarytStatsTop} data-test-subj="beatSummaryStatus01" />
|
||||
<SummaryStatus
|
||||
metrics={summarytStatsTop}
|
||||
alerts={alerts}
|
||||
data-test-subj="beatSummaryStatus01"
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel>
|
||||
<SummaryStatus metrics={summarytStatsBot} data-test-subj="beatSummaryStatus02" />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<AlertsCallout
|
||||
alerts={alerts}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Beat instances')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<EuiPageContent>
|
||||
<EuiScreenReaderOnly>
|
||||
<h1>
|
||||
|
|
|
@ -26,10 +26,12 @@ import { SetupModeBadge } from '../../setup_mode/badge';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
|
||||
import { SetupModeFeature } from '../../../../common/enums';
|
||||
import { AlertsStatus } from '../../../alerts/status';
|
||||
|
||||
export class Listing extends PureComponent {
|
||||
getColumns() {
|
||||
const setupMode = this.props.setupMode;
|
||||
const alerts = this.props.alerts;
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -72,6 +74,29 @@ export class Listing extends PureComponent {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.monitoring.beats.instances.alertsColumnTitle', {
|
||||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
field: 'alerts',
|
||||
width: '175px',
|
||||
sortable: true,
|
||||
render: (_field, beat) => {
|
||||
return (
|
||||
<AlertsStatus
|
||||
showBadge={true}
|
||||
alerts={alerts}
|
||||
stateFilter={(state) => state.stackProductUuid === beat.uuid}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Beat instances')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.monitoring.beats.instances.typeTitle', {
|
||||
defaultMessage: 'Type',
|
||||
|
@ -122,7 +147,7 @@ export class Listing extends PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { stats, data, sorting, pagination, onTableChange, setupMode } = this.props;
|
||||
const { stats, data, sorting, pagination, onTableChange, setupMode, alerts } = this.props;
|
||||
|
||||
let setupModeCallOut = null;
|
||||
if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) {
|
||||
|
@ -155,7 +180,7 @@ export class Listing extends PureComponent {
|
|||
</h1>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiPanel>
|
||||
<Stats stats={stats} />
|
||||
<Stats stats={stats} alerts={alerts} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPageContent>
|
||||
|
|
|
@ -84,6 +84,7 @@ export function BeatsOverview({
|
|||
latestVersions,
|
||||
stats,
|
||||
metrics,
|
||||
alerts,
|
||||
...props
|
||||
}) {
|
||||
const seriesToShow = [
|
||||
|
@ -113,7 +114,7 @@ export function BeatsOverview({
|
|||
</h1>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiPanel>
|
||||
<Stats stats={stats} />
|
||||
<Stats stats={stats} alerts={alerts} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel>{renderLatestActive(latestActive, latestTypes, latestVersions)}</EuiPanel>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { formatMetric } from '../../lib/format_number';
|
|||
import { SummaryStatus } from '../summary_status';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function Stats({ stats }) {
|
||||
export function Stats({ stats, alerts }) {
|
||||
const {
|
||||
total,
|
||||
types,
|
||||
|
@ -51,5 +51,5 @@ export function Stats({ stats }) {
|
|||
'data-test-subj': 'bytesSent',
|
||||
});
|
||||
|
||||
return <SummaryStatus metrics={metrics} data-test-subj="beatsSummaryStatus" />;
|
||||
return <SummaryStatus metrics={metrics} alerts={alerts} data-test-subj="beatsSummaryStatus" />;
|
||||
}
|
||||
|
|
|
@ -24,14 +24,22 @@ import {
|
|||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { formatTimestampToDuration } from '../../../../common';
|
||||
import { CALCULATE_DURATION_SINCE, APM_SYSTEM_ID } from '../../../../common/constants';
|
||||
import {
|
||||
CALCULATE_DURATION_SINCE,
|
||||
APM_SYSTEM_ID,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
} from '../../../../common/constants';
|
||||
import { SetupModeTooltip } from '../../setup_mode/tooltip';
|
||||
import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
|
||||
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
|
||||
import { SetupModeFeature } from '../../../../common/enums';
|
||||
import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
|
||||
import { AlertsBadge } from '../../../alerts/badge';
|
||||
|
||||
const SERVERS_PANEL_ALERTS = [ALERT_MISSING_MONITORING_DATA];
|
||||
|
||||
export function ApmPanel(props) {
|
||||
const { setupMode } = props;
|
||||
const { setupMode, alerts } = props;
|
||||
const apmsTotal = get(props, 'apms.total') || 0;
|
||||
// Do not show if we are not in setup mode
|
||||
if (apmsTotal === 0 && !setupMode.enabled) {
|
||||
|
@ -50,6 +58,16 @@ export function ApmPanel(props) {
|
|||
/>
|
||||
) : null;
|
||||
|
||||
let apmServersAlertStatus = null;
|
||||
if (shouldShowAlertBadge(alerts, SERVERS_PANEL_ALERTS)) {
|
||||
const alertsList = SERVERS_PANEL_ALERTS.map((alertType) => alerts[alertType]);
|
||||
apmServersAlertStatus = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsBadge alerts={alertsList} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ClusterItemContainer
|
||||
{...props}
|
||||
|
@ -140,7 +158,12 @@ export function ApmPanel(props) {
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{setupModeMetricbeatMigrationTooltip}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{setupModeMetricbeatMigrationTooltip}
|
||||
{apmServersAlertStatus}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiDescriptionList type="column">
|
||||
|
|
|
@ -23,13 +23,17 @@ import { ClusterItemContainer, DisabledIfNoDataAndInSetupModeLink } from './help
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SetupModeTooltip } from '../../setup_mode/tooltip';
|
||||
import { BEATS_SYSTEM_ID } from '../../../../common/constants';
|
||||
import { ALERT_MISSING_MONITORING_DATA, BEATS_SYSTEM_ID } from '../../../../common/constants';
|
||||
import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
|
||||
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
|
||||
import { SetupModeFeature } from '../../../../common/enums';
|
||||
import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
|
||||
import { AlertsBadge } from '../../../alerts/badge';
|
||||
|
||||
const BEATS_PANEL_ALERTS = [ALERT_MISSING_MONITORING_DATA];
|
||||
|
||||
export function BeatsPanel(props) {
|
||||
const { setupMode } = props;
|
||||
const { setupMode, alerts } = props;
|
||||
const beatsTotal = get(props, 'beats.total') || 0;
|
||||
// Do not show if we are not in setup mode
|
||||
if (beatsTotal === 0 && !setupMode.enabled) {
|
||||
|
@ -47,6 +51,16 @@ export function BeatsPanel(props) {
|
|||
/>
|
||||
) : null;
|
||||
|
||||
let beatsAlertsStatus = null;
|
||||
if (shouldShowAlertBadge(alerts, BEATS_PANEL_ALERTS)) {
|
||||
const alertsList = BEATS_PANEL_ALERTS.map((alertType) => alerts[alertType]);
|
||||
beatsAlertsStatus = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsBadge alerts={alertsList} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
const beatTypes = props.beats.types.map((beat, index) => {
|
||||
return [
|
||||
<EuiDescriptionListTitle
|
||||
|
@ -145,7 +159,12 @@ export function BeatsPanel(props) {
|
|||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{setupModeMetricbeatMigrationTooltip}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{setupModeMetricbeatMigrationTooltip}
|
||||
{beatsAlertsStatus}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiDescriptionList type="column">{beatTypes}</EuiDescriptionList>
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
ALERT_DISK_USAGE,
|
||||
ALERT_NODES_CHANGED,
|
||||
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
} from '../../../../common/constants';
|
||||
import { AlertsBadge } from '../../../alerts/badge';
|
||||
import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
|
||||
|
@ -161,6 +162,7 @@ const NODES_PANEL_ALERTS = [
|
|||
ALERT_DISK_USAGE,
|
||||
ALERT_NODES_CHANGED,
|
||||
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
];
|
||||
|
||||
export function ElasticsearchPanel(props) {
|
||||
|
|
|
@ -12,7 +12,16 @@ import { BeatsPanel } from './beats_panel';
|
|||
import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import { ApmPanel } from './apm_panel';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants';
|
||||
import {
|
||||
STANDALONE_CLUSTER_CLUSTER_UUID,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
ELASTICSEARCH_SYSTEM_ID,
|
||||
KIBANA_SYSTEM_ID,
|
||||
LOGSTASH_SYSTEM_ID,
|
||||
BEATS_SYSTEM_ID,
|
||||
APM_SYSTEM_ID,
|
||||
} from '../../../../common/constants';
|
||||
import { filterAlertStates } from '../../../alerts/filter_alert_states';
|
||||
|
||||
export function Overview(props) {
|
||||
const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID;
|
||||
|
@ -37,12 +46,22 @@ export function Overview(props) {
|
|||
license={props.cluster.license}
|
||||
setupMode={props.setupMode}
|
||||
showLicenseExpiration={props.showLicenseExpiration}
|
||||
alerts={props.alerts}
|
||||
alerts={filterAlertStates(props.alerts, (type, { state }) => {
|
||||
if (type === ALERT_MISSING_MONITORING_DATA) {
|
||||
return state.stackProduct === ELASTICSEARCH_SYSTEM_ID;
|
||||
}
|
||||
return true;
|
||||
})}
|
||||
/>
|
||||
<KibanaPanel
|
||||
{...props.cluster.kibana}
|
||||
setupMode={props.setupMode}
|
||||
alerts={props.alerts}
|
||||
alerts={filterAlertStates(props.alerts, (type, { state }) => {
|
||||
if (type === ALERT_MISSING_MONITORING_DATA) {
|
||||
return state.stackProduct === KIBANA_SYSTEM_ID;
|
||||
}
|
||||
return true;
|
||||
})}
|
||||
/>
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
@ -50,12 +69,35 @@ export function Overview(props) {
|
|||
<LogstashPanel
|
||||
{...props.cluster.logstash}
|
||||
setupMode={props.setupMode}
|
||||
alerts={props.alerts}
|
||||
alerts={filterAlertStates(props.alerts, (type, { state }) => {
|
||||
if (type === ALERT_MISSING_MONITORING_DATA) {
|
||||
return state.stackProduct === LOGSTASH_SYSTEM_ID;
|
||||
}
|
||||
return true;
|
||||
})}
|
||||
/>
|
||||
|
||||
<BeatsPanel {...props.cluster.beats} setupMode={props.setupMode} />
|
||||
<BeatsPanel
|
||||
{...props.cluster.beats}
|
||||
setupMode={props.setupMode}
|
||||
alerts={filterAlertStates(props.alerts, (type, { state }) => {
|
||||
if (type === ALERT_MISSING_MONITORING_DATA) {
|
||||
return state.stackProduct === BEATS_SYSTEM_ID;
|
||||
}
|
||||
return true;
|
||||
})}
|
||||
/>
|
||||
|
||||
<ApmPanel {...props.cluster.apm} setupMode={props.setupMode} />
|
||||
<ApmPanel
|
||||
{...props.cluster.apm}
|
||||
setupMode={props.setupMode}
|
||||
alerts={filterAlertStates(props.alerts, (type, { state }) => {
|
||||
if (type === ALERT_MISSING_MONITORING_DATA) {
|
||||
return state.stackProduct === APM_SYSTEM_ID;
|
||||
}
|
||||
return true;
|
||||
})}
|
||||
/>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
|
|
|
@ -28,14 +28,18 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SetupModeTooltip } from '../../setup_mode/tooltip';
|
||||
import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants';
|
||||
import {
|
||||
KIBANA_SYSTEM_ID,
|
||||
ALERT_KIBANA_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
} from '../../../../common/constants';
|
||||
import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
|
||||
import { AlertsBadge } from '../../../alerts/badge';
|
||||
import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge';
|
||||
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
|
||||
import { SetupModeFeature } from '../../../../common/enums';
|
||||
|
||||
const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH];
|
||||
const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA];
|
||||
|
||||
export function KibanaPanel(props) {
|
||||
const setupMode = props.setupMode;
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
LOGSTASH,
|
||||
LOGSTASH_SYSTEM_ID,
|
||||
ALERT_LOGSTASH_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
import {
|
||||
|
@ -40,7 +41,7 @@ import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badg
|
|||
import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode';
|
||||
import { SetupModeFeature } from '../../../../common/enums';
|
||||
|
||||
const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH];
|
||||
const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA];
|
||||
|
||||
export function LogstashPanel(props) {
|
||||
const { setupMode } = props;
|
||||
|
|
|
@ -73,11 +73,22 @@ export const Node = ({
|
|||
<NodeDetailStatus
|
||||
stats={nodeSummary}
|
||||
alerts={alerts}
|
||||
alertsStateFilter={(state) => state.nodeId === nodeId}
|
||||
alertsStateFilter={(state) =>
|
||||
state.nodeId === nodeId || state.stackProductUuid === nodeId
|
||||
}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<AlertsCallout alerts={alerts} stateFilter={(state) => state.nodeId === nodeId} />
|
||||
<AlertsCallout
|
||||
alerts={alerts}
|
||||
stateFilter={(state) => state.nodeId === nodeId || state.stackProductUuid === nodeId}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Elasticsearch nodes')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<EuiPageContent>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
{metricsToShow.map((metric, index) => (
|
||||
|
|
|
@ -137,7 +137,15 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler
|
|||
<AlertsStatus
|
||||
showBadge={true}
|
||||
alerts={alerts}
|
||||
stateFilter={(state) => state.nodeId === node.resolver}
|
||||
stateFilter={(state) =>
|
||||
state.nodeId === node.resolver || state.stackProductUuid === node.resolver
|
||||
}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Elasticsearch nodes')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -91,7 +91,18 @@ const getColumns = (setupMode, alerts) => {
|
|||
width: '175px',
|
||||
sortable: true,
|
||||
render: () => {
|
||||
return <AlertsStatus showBadge={true} alerts={alerts} />;
|
||||
return (
|
||||
<AlertsStatus
|
||||
showBadge={true}
|
||||
alerts={alerts}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Kibana instances')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -84,7 +84,18 @@ export class Listing extends PureComponent {
|
|||
width: '175px',
|
||||
sortable: true,
|
||||
render: () => {
|
||||
return <AlertsStatus showBadge={true} alerts={alerts} />;
|
||||
return (
|
||||
<AlertsStatus
|
||||
showBadge={true}
|
||||
alerts={alerts}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Logstash nodes')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -23,6 +23,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
|
|||
import { MonitoringStartPluginDependencies, MonitoringConfig } from './types';
|
||||
import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public';
|
||||
import { createCpuUsageAlertType } from './alerts/cpu_usage_alert';
|
||||
import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert';
|
||||
import { createLegacyAlertTypes } from './alerts/legacy_alert';
|
||||
import { createDiskUsageAlertType } from './alerts/disk_usage_alert';
|
||||
|
||||
|
@ -72,6 +73,7 @@ export class MonitoringPlugin
|
|||
}
|
||||
|
||||
plugins.triggers_actions_ui.alertTypeRegistry.register(createCpuUsageAlertType());
|
||||
plugins.triggers_actions_ui.alertTypeRegistry.register(createMissingMonitoringDataAlertType());
|
||||
plugins.triggers_actions_ui.alertTypeRegistry.register(createDiskUsageAlertType());
|
||||
const legacyAlertTypes = createLegacyAlertTypes();
|
||||
for (const legacyAlertType of legacyAlertTypes) {
|
||||
|
|
|
@ -18,7 +18,11 @@ import { routeInitProvider } from '../../../lib/route_init';
|
|||
import template from './index.html';
|
||||
import { MonitoringViewBaseController } from '../../base_controller';
|
||||
import { ApmServerInstance } from '../../../components/apm/instance';
|
||||
import { CODE_PATH_APM } from '../../../../common/constants';
|
||||
import {
|
||||
CODE_PATH_APM,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
APM_SYSTEM_ID,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
uiRoutes.when('/apm/instances/:uuid', {
|
||||
template,
|
||||
|
@ -50,6 +54,17 @@ uiRoutes.when('/apm/instances/:uuid', {
|
|||
reactNodeId: 'apmInstanceReact',
|
||||
$scope,
|
||||
$injector,
|
||||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: APM_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$scope.$watch(
|
||||
|
@ -69,6 +84,7 @@ uiRoutes.when('/apm/instances/:uuid', {
|
|||
summary={data.apmSummary || {}}
|
||||
metrics={data.metrics || {}}
|
||||
onBrush={this.onBrush}
|
||||
alerts={this.alerts}
|
||||
zoomInfo={this.zoomInfo}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -13,7 +13,11 @@ import template from './index.html';
|
|||
import { ApmServerInstances } from '../../../components/apm/instances';
|
||||
import { MonitoringViewBaseEuiTableController } from '../..';
|
||||
import { SetupModeRenderer } from '../../../components/renderers';
|
||||
import { APM_SYSTEM_ID, CODE_PATH_APM } from '../../../../common/constants';
|
||||
import {
|
||||
APM_SYSTEM_ID,
|
||||
CODE_PATH_APM,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
uiRoutes.when('/apm/instances', {
|
||||
template,
|
||||
|
@ -47,6 +51,17 @@ uiRoutes.when('/apm/instances', {
|
|||
reactNodeId: 'apmInstancesReact',
|
||||
$scope,
|
||||
$injector,
|
||||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: APM_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.scope = $scope;
|
||||
|
@ -67,6 +82,7 @@ uiRoutes.when('/apm/instances', {
|
|||
{flyoutComponent}
|
||||
<ApmServerInstances
|
||||
setupMode={setupMode}
|
||||
alerts={this.alerts}
|
||||
apms={{
|
||||
pagination,
|
||||
sorting,
|
||||
|
|
|
@ -12,7 +12,11 @@ import { routeInitProvider } from '../../../lib/route_init';
|
|||
import template from './index.html';
|
||||
import { MonitoringViewBaseController } from '../../base_controller';
|
||||
import { ApmOverview } from '../../../components/apm/overview';
|
||||
import { CODE_PATH_APM } from '../../../../common/constants';
|
||||
import {
|
||||
CODE_PATH_APM,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
APM_SYSTEM_ID,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
uiRoutes.when('/apm', {
|
||||
template,
|
||||
|
@ -42,13 +46,29 @@ uiRoutes.when('/apm', {
|
|||
reactNodeId: 'apmOverviewReact',
|
||||
$scope,
|
||||
$injector,
|
||||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: APM_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
$scope.$watch(
|
||||
() => this.data,
|
||||
(data) => {
|
||||
this.renderReact(
|
||||
<ApmOverview {...data} onBrush={this.onBrush} zoomInfo={this.zoomInfo} />
|
||||
<ApmOverview
|
||||
alerts={this.alerts}
|
||||
{...data}
|
||||
onBrush={this.onBrush}
|
||||
zoomInfo={this.zoomInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -11,7 +11,11 @@ import { routeInitProvider } from '../../../lib/route_init';
|
|||
import { MonitoringViewBaseController } from '../../';
|
||||
import { getPageData } from './get_page_data';
|
||||
import template from './index.html';
|
||||
import { CODE_PATH_BEATS } from '../../../../common/constants';
|
||||
import {
|
||||
CODE_PATH_BEATS,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
BEATS_SYSTEM_ID,
|
||||
} from '../../../../common/constants';
|
||||
import { Beat } from '../../../components/beats/beat';
|
||||
|
||||
uiRoutes.when('/beats/beat/:beatUuid', {
|
||||
|
@ -52,6 +56,17 @@ uiRoutes.when('/beats/beat/:beatUuid', {
|
|||
$scope,
|
||||
$injector,
|
||||
reactNodeId: 'monitoringBeatsInstanceApp',
|
||||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: BEATS_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.data = pageData;
|
||||
|
@ -60,6 +75,7 @@ uiRoutes.when('/beats/beat/:beatUuid', {
|
|||
(data) => {
|
||||
this.renderReact(
|
||||
<Beat
|
||||
alerts={this.alerts}
|
||||
summary={data.summary}
|
||||
metrics={data.metrics}
|
||||
onBrush={$scope.onBrush}
|
||||
|
|
|
@ -14,7 +14,11 @@ import template from './index.html';
|
|||
import React, { Fragment } from 'react';
|
||||
import { Listing } from '../../../components/beats/listing/listing';
|
||||
import { SetupModeRenderer } from '../../../components/renderers';
|
||||
import { CODE_PATH_BEATS, BEATS_SYSTEM_ID } from '../../../../common/constants';
|
||||
import {
|
||||
CODE_PATH_BEATS,
|
||||
BEATS_SYSTEM_ID,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
uiRoutes.when('/beats/beats', {
|
||||
template,
|
||||
|
@ -46,6 +50,17 @@ uiRoutes.when('/beats/beats', {
|
|||
reactNodeId: 'monitoringBeatsInstancesApp',
|
||||
$scope,
|
||||
$injector,
|
||||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: BEATS_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.data = $route.current.locals.pageData;
|
||||
|
@ -71,6 +86,7 @@ uiRoutes.when('/beats/beats', {
|
|||
<Listing
|
||||
stats={this.data.stats}
|
||||
data={this.data.listing}
|
||||
alerts={this.alerts}
|
||||
setupMode={setupMode}
|
||||
sorting={this.sorting || sorting}
|
||||
pagination={this.pagination || pagination}
|
||||
|
|
|
@ -11,7 +11,11 @@ import { routeInitProvider } from '../../../lib/route_init';
|
|||
import { MonitoringViewBaseController } from '../../';
|
||||
import { getPageData } from './get_page_data';
|
||||
import template from './index.html';
|
||||
import { CODE_PATH_BEATS } from '../../../../common/constants';
|
||||
import {
|
||||
CODE_PATH_BEATS,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
BEATS_SYSTEM_ID,
|
||||
} from '../../../../common/constants';
|
||||
import { BeatsOverview } from '../../../components/beats/overview';
|
||||
|
||||
uiRoutes.when('/beats', {
|
||||
|
@ -44,6 +48,17 @@ uiRoutes.when('/beats', {
|
|||
$scope,
|
||||
$injector,
|
||||
reactNodeId: 'monitoringBeatsOverviewApp',
|
||||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: BEATS_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.data = $route.current.locals.pageData;
|
||||
|
@ -51,7 +66,12 @@ uiRoutes.when('/beats', {
|
|||
() => this.data,
|
||||
(data) => {
|
||||
this.renderReact(
|
||||
<BeatsOverview {...data} onBrush={$scope.onBrush} zoomInfo={$scope.zoomInfo} />
|
||||
<BeatsOverview
|
||||
{...data}
|
||||
alerts={this.alerts}
|
||||
onBrush={$scope.onBrush}
|
||||
zoomInfo={$scope.zoomInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ import { MonitoringViewBaseController } from '../../../base_controller';
|
|||
import {
|
||||
CODE_PATH_ELASTICSEARCH,
|
||||
ALERT_CPU_USAGE,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
ALERT_DISK_USAGE,
|
||||
} from '../../../../../common/constants';
|
||||
|
||||
|
@ -71,7 +72,7 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', {
|
|||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE],
|
||||
alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
nodeUuid: nodeName,
|
||||
|
|
|
@ -21,6 +21,7 @@ import { MonitoringViewBaseController } from '../../base_controller';
|
|||
import {
|
||||
CODE_PATH_ELASTICSEARCH,
|
||||
ALERT_CPU_USAGE,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
ALERT_DISK_USAGE,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
|
@ -55,7 +56,7 @@ uiRoutes.when('/elasticsearch/nodes/:node', {
|
|||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE],
|
||||
alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
nodeUuid: nodeName,
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
ELASTICSEARCH_SYSTEM_ID,
|
||||
CODE_PATH_ELASTICSEARCH,
|
||||
ALERT_CPU_USAGE,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
ALERT_DISK_USAGE,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
|
@ -87,7 +88,12 @@ uiRoutes.when('/elasticsearch/nodes', {
|
|||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE],
|
||||
alertTypeIds: [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: ELASTICSEARCH_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -27,7 +27,12 @@ import {
|
|||
import { MonitoringTimeseriesContainer } from '../../../components/chart';
|
||||
import { DetailStatus } from '../../../components/kibana/detail_status';
|
||||
import { MonitoringViewBaseController } from '../../base_controller';
|
||||
import { CODE_PATH_KIBANA, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants';
|
||||
import {
|
||||
CODE_PATH_KIBANA,
|
||||
ALERT_KIBANA_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
KIBANA_SYSTEM_ID,
|
||||
} from '../../../../common/constants';
|
||||
import { AlertsCallout } from '../../../alerts/callout';
|
||||
|
||||
function getPageData($injector) {
|
||||
|
@ -76,7 +81,12 @@ uiRoutes.when('/kibana/instances/:uuid', {
|
|||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH],
|
||||
alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: KIBANA_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -104,7 +114,15 @@ uiRoutes.when('/kibana/instances/:uuid', {
|
|||
<DetailStatus stats={data.kibanaSummary} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<AlertsCallout alerts={this.alerts} />
|
||||
<AlertsCallout
|
||||
alerts={this.alerts}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Kibana instances')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<EuiPageContent>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem grow={true}>
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
KIBANA_SYSTEM_ID,
|
||||
CODE_PATH_KIBANA,
|
||||
ALERT_KIBANA_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
uiRoutes.when('/kibana/instances', {
|
||||
|
@ -46,7 +47,12 @@ uiRoutes.when('/kibana/instances', {
|
|||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH],
|
||||
alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: KIBANA_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -26,7 +26,13 @@ import {
|
|||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { MonitoringTimeseriesContainer } from '../../../../components/chart';
|
||||
import { CODE_PATH_LOGSTASH } from '../../../../../common/constants';
|
||||
import {
|
||||
CODE_PATH_LOGSTASH,
|
||||
ALERT_LOGSTASH_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
LOGSTASH_SYSTEM_ID,
|
||||
} from '../../../../../common/constants';
|
||||
import { AlertsCallout } from '../../../../alerts/callout';
|
||||
|
||||
function getPageData($injector) {
|
||||
const $http = $injector.get('$http');
|
||||
|
@ -69,6 +75,17 @@ uiRoutes.when('/logstash/node/:uuid/advanced', {
|
|||
reactNodeId: 'monitoringLogstashNodeAdvancedApp',
|
||||
$scope,
|
||||
$injector,
|
||||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: LOGSTASH_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
telemetryPageViewTitle: 'logstash_node_advanced',
|
||||
});
|
||||
|
||||
|
@ -112,6 +129,15 @@ uiRoutes.when('/logstash/node/:uuid/advanced', {
|
|||
<DetailStatus stats={data.nodeSummary} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<AlertsCallout
|
||||
alerts={this.alerts}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Logstash nodes')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<EuiPageContent>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
{metricsToShow.map((metric, index) => (
|
||||
|
|
|
@ -26,7 +26,12 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { MonitoringTimeseriesContainer } from '../../../components/chart';
|
||||
import { MonitoringViewBaseController } from '../../base_controller';
|
||||
import { CODE_PATH_LOGSTASH, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants';
|
||||
import {
|
||||
CODE_PATH_LOGSTASH,
|
||||
ALERT_LOGSTASH_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
LOGSTASH_SYSTEM_ID,
|
||||
} from '../../../../common/constants';
|
||||
import { AlertsCallout } from '../../../alerts/callout';
|
||||
|
||||
function getPageData($injector) {
|
||||
|
@ -73,7 +78,12 @@ uiRoutes.when('/logstash/node/:uuid', {
|
|||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH],
|
||||
alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: LOGSTASH_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
telemetryPageViewTitle: 'logstash_node',
|
||||
|
@ -120,7 +130,15 @@ uiRoutes.when('/logstash/node/:uuid', {
|
|||
<DetailStatus stats={data.nodeSummary} />
|
||||
</EuiPanel>
|
||||
<EuiSpacer size="m" />
|
||||
<AlertsCallout alerts={this.alerts} />
|
||||
<AlertsCallout
|
||||
alerts={this.alerts}
|
||||
nextStepsFilter={(nextStep) => {
|
||||
if (nextStep.text.includes('Logstash nodes')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<EuiPageContent>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
{metricsToShow.map((metric, index) => (
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
CODE_PATH_LOGSTASH,
|
||||
LOGSTASH_SYSTEM_ID,
|
||||
ALERT_LOGSTASH_VERSION_MISMATCH,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
uiRoutes.when('/logstash/nodes', {
|
||||
|
@ -45,7 +46,12 @@ uiRoutes.when('/logstash/nodes', {
|
|||
alerts: {
|
||||
shouldFetch: true,
|
||||
options: {
|
||||
alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH],
|
||||
alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH, ALERT_MISSING_MONITORING_DATA],
|
||||
filters: [
|
||||
{
|
||||
stackProduct: LOGSTASH_SYSTEM_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -63,6 +63,6 @@ describe('AlertsFactory', () => {
|
|||
|
||||
it('should get all', () => {
|
||||
const alerts = AlertsFactory.getAll();
|
||||
expect(alerts.length).toBe(8);
|
||||
expect(alerts.length).toBe(9);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import {
|
||||
CpuUsageAlert,
|
||||
MissingMonitoringDataAlert,
|
||||
DiskUsageAlert,
|
||||
NodesChangedAlert,
|
||||
ClusterHealthAlert,
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
ALERT_CLUSTER_HEALTH,
|
||||
ALERT_LICENSE_EXPIRATION,
|
||||
ALERT_CPU_USAGE,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
ALERT_DISK_USAGE,
|
||||
ALERT_NODES_CHANGED,
|
||||
ALERT_LOGSTASH_VERSION_MISMATCH,
|
||||
|
@ -31,6 +33,7 @@ export const BY_TYPE = {
|
|||
[ALERT_CLUSTER_HEALTH]: ClusterHealthAlert,
|
||||
[ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert,
|
||||
[ALERT_CPU_USAGE]: CpuUsageAlert,
|
||||
[ALERT_MISSING_MONITORING_DATA]: MissingMonitoringDataAlert,
|
||||
[ALERT_DISK_USAGE]: DiskUsageAlert,
|
||||
[ALERT_NODES_CHANGED]: NodesChangedAlert,
|
||||
[ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert,
|
||||
|
|
|
@ -198,6 +198,15 @@ export class BaseAlert {
|
|||
const alertInstance: RawAlertInstance = states.alertInstances[instanceId];
|
||||
if (alertInstance && this.filterAlertInstance(alertInstance, filters)) {
|
||||
accum[instanceId] = alertInstance;
|
||||
if (alertInstance.state) {
|
||||
accum[instanceId].state = {
|
||||
alertStates: (alertInstance.state as AlertInstanceState).alertStates.filter(
|
||||
(alertState: AlertState) => {
|
||||
return this.filterAlertState(alertState, filters);
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
return accum;
|
||||
},
|
||||
|
@ -209,6 +218,10 @@ export class BaseAlert {
|
|||
return true;
|
||||
}
|
||||
|
||||
protected filterAlertState(alertState: AlertState, filters: CommonAlertFilter[]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async execute({ services, params, state }: AlertExecutorOptions): Promise<any> {
|
||||
const logger = this.getLogger(this.type);
|
||||
logger.debug(
|
||||
|
@ -226,13 +239,7 @@ export class BaseAlert {
|
|||
return await mbSafeQuery(async () => _callCluster(endpoint, clientParams, options));
|
||||
};
|
||||
const availableCcs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : [];
|
||||
// Support CCS use cases by querying to find available remote clusters
|
||||
// and then adding those to the index pattern we are searching against
|
||||
let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH);
|
||||
if (availableCcs) {
|
||||
esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs);
|
||||
}
|
||||
const clusters = await fetchClusters(callCluster, esIndexPattern);
|
||||
const clusters = await this.fetchClusters(callCluster, availableCcs, params);
|
||||
const uiSettings = (await this.getUiSettingsService()).asScopedToClient(
|
||||
services.savedObjectsClient
|
||||
);
|
||||
|
@ -241,6 +248,26 @@ export class BaseAlert {
|
|||
return await this.processData(data, clusters, services, logger, state);
|
||||
}
|
||||
|
||||
protected async fetchClusters(
|
||||
callCluster: any,
|
||||
availableCcs: string[] | undefined = undefined,
|
||||
params: CommonAlertParams
|
||||
) {
|
||||
let ccs;
|
||||
if (!availableCcs) {
|
||||
ccs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : undefined;
|
||||
} else {
|
||||
ccs = availableCcs;
|
||||
}
|
||||
// Support CCS use cases by querying to find available remote clusters
|
||||
// and then adding those to the index pattern we are searching against
|
||||
let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH);
|
||||
if (ccs) {
|
||||
esIndexPattern = getCcsIndexPattern(esIndexPattern, ccs);
|
||||
}
|
||||
return await fetchClusters(callCluster, esIndexPattern);
|
||||
}
|
||||
|
||||
protected async fetchData(
|
||||
params: CommonAlertParams,
|
||||
callCluster: any,
|
||||
|
|
|
@ -26,7 +26,7 @@ import { RawAlertInstance } from '../../../alerts/common';
|
|||
import { parseDuration } from '../../../alerts/common/parse_duration';
|
||||
import {
|
||||
CommonAlertFilter,
|
||||
CommonAlertCpuUsageFilter,
|
||||
CommonAlertNodeUuidFilter,
|
||||
CommonAlertParams,
|
||||
CommonAlertParamDetail,
|
||||
} from '../../common/types';
|
||||
|
@ -129,7 +129,7 @@ export class CpuUsageAlert extends BaseAlert {
|
|||
const alertInstanceState = (alertInstance.state as unknown) as AlertInstanceState;
|
||||
if (filters && filters.length) {
|
||||
for (const _filter of filters) {
|
||||
const filter = _filter as CommonAlertCpuUsageFilter;
|
||||
const filter = _filter as CommonAlertNodeUuidFilter;
|
||||
if (filter && filter.nodeUuid) {
|
||||
let nodeExistsInStates = false;
|
||||
for (const state of alertInstanceState.alertStates) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
export { BaseAlert } from './base_alert';
|
||||
export { CpuUsageAlert } from './cpu_usage_alert';
|
||||
export { MissingMonitoringDataAlert } from './missing_monitoring_data_alert';
|
||||
export { DiskUsageAlert } from './disk_usage_alert';
|
||||
export { ClusterHealthAlert } from './cluster_health_alert';
|
||||
export { LicenseExpirationAlert } from './license_expiration_alert';
|
||||
|
|
|
@ -0,0 +1,459 @@
|
|||
/*
|
||||
* 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 { MissingMonitoringDataAlert } from './missing_monitoring_data_alert';
|
||||
import { ALERT_MISSING_MONITORING_DATA } from '../../common/constants';
|
||||
import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
|
||||
const RealDate = Date;
|
||||
|
||||
jest.mock('../lib/alerts/fetch_missing_monitoring_data', () => ({
|
||||
fetchMissingMonitoringData: jest.fn(),
|
||||
}));
|
||||
jest.mock('../lib/alerts/fetch_clusters', () => ({
|
||||
fetchClusters: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('MissingMonitoringDataAlert', () => {
|
||||
it('should have defaults', () => {
|
||||
const alert = new MissingMonitoringDataAlert();
|
||||
expect(alert.type).toBe(ALERT_MISSING_MONITORING_DATA);
|
||||
expect(alert.label).toBe('Missing monitoring data');
|
||||
expect(alert.defaultThrottle).toBe('1d');
|
||||
// @ts-ignore
|
||||
expect(alert.defaultParams).toStrictEqual({ limit: '1d', duration: '5m' });
|
||||
// @ts-ignore
|
||||
expect(alert.actionVariables).toStrictEqual([
|
||||
{ name: 'stackProducts', description: 'The stack products missing monitoring data.' },
|
||||
{ name: 'count', description: 'The number of stack products missing monitoring data.' },
|
||||
{
|
||||
name: 'internalShortMessage',
|
||||
description: 'The short internal message generated by Elastic.',
|
||||
},
|
||||
{
|
||||
name: 'internalFullMessage',
|
||||
description: 'The full internal message generated by Elastic.',
|
||||
},
|
||||
{ name: 'state', description: 'The current state of the alert.' },
|
||||
{ name: 'clusterName', description: 'The cluster to which the nodes belong.' },
|
||||
{ name: 'action', description: 'The recommended action for this alert.' },
|
||||
{
|
||||
name: 'actionPlain',
|
||||
description: 'The recommended action for this alert, without any markdown.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
function FakeDate() {}
|
||||
FakeDate.prototype.valueOf = () => 1;
|
||||
|
||||
const clusterUuid = 'abc123';
|
||||
const clusterName = 'testCluster';
|
||||
const stackProduct = 'elasticsearch';
|
||||
const stackProductUuid = 'esNode1';
|
||||
const stackProductName = 'esName1';
|
||||
const gapDuration = 3000001;
|
||||
const missingData = [
|
||||
{
|
||||
stackProduct,
|
||||
stackProductUuid,
|
||||
stackProductName,
|
||||
clusterUuid,
|
||||
gapDuration,
|
||||
},
|
||||
{
|
||||
stackProduct: 'kibana',
|
||||
stackProductUuid: 'kibanaUuid1',
|
||||
stackProductName: 'kibanaInstance1',
|
||||
clusterUuid,
|
||||
gapDuration: gapDuration + 10,
|
||||
},
|
||||
];
|
||||
const getUiSettingsService = () => ({
|
||||
asScopedToClient: jest.fn(),
|
||||
});
|
||||
const getLogger = () => ({
|
||||
debug: jest.fn(),
|
||||
});
|
||||
const monitoringCluster = null;
|
||||
const config = {
|
||||
ui: {
|
||||
ccs: { enabled: true },
|
||||
container: { elasticsearch: { enabled: false } },
|
||||
metricbeat: { index: 'metricbeat-*' },
|
||||
},
|
||||
};
|
||||
const kibanaUrl = 'http://localhost:5601';
|
||||
|
||||
const replaceState = jest.fn();
|
||||
const scheduleActions = jest.fn();
|
||||
const getState = jest.fn();
|
||||
const executorOptions = {
|
||||
services: {
|
||||
callCluster: jest.fn(),
|
||||
alertInstanceFactory: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
replaceState,
|
||||
scheduleActions,
|
||||
getState,
|
||||
};
|
||||
}),
|
||||
},
|
||||
state: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
Date = FakeDate;
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return missingData;
|
||||
});
|
||||
(fetchClusters as jest.Mock).mockImplementation(() => {
|
||||
return [{ clusterUuid, clusterName }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Date = RealDate;
|
||||
replaceState.mockReset();
|
||||
scheduleActions.mockReset();
|
||||
getState.mockReset();
|
||||
});
|
||||
|
||||
it('should fire actions', async () => {
|
||||
const alert = new MissingMonitoringDataAlert();
|
||||
alert.initializeAlertType(
|
||||
getUiSettingsService as any,
|
||||
monitoringCluster as any,
|
||||
getLogger as any,
|
||||
config as any,
|
||||
kibanaUrl,
|
||||
false
|
||||
);
|
||||
const type = alert.getAlertType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
// @ts-ignore
|
||||
params: alert.defaultParams,
|
||||
} as any);
|
||||
const count = 2;
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
gapDuration,
|
||||
stackProduct,
|
||||
stackProductName,
|
||||
stackProductUuid,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text:
|
||||
'For the past an hour, we have not detected any monitoring data from the Elasticsearch node: esName1, starting at #absolute',
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkView all Elasticsearch nodes#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'elasticsearch/nodes',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Verify monitoring settings on the node',
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
resolvedMS: 0,
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
ccs: undefined,
|
||||
cluster: { clusterUuid, clusterName },
|
||||
gapDuration: gapDuration + 10,
|
||||
stackProduct: 'kibana',
|
||||
stackProductName: 'kibanaInstance1',
|
||||
stackProductUuid: 'kibanaUuid1',
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: {
|
||||
text:
|
||||
'For the past an hour, we have not detected any monitoring data from the Kibana instance: kibanaInstance1, starting at #absolute',
|
||||
nextSteps: [
|
||||
{
|
||||
text: '#start_linkView all Kibana instances#end_link',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#start_link',
|
||||
endToken: '#end_link',
|
||||
type: 'link',
|
||||
url: 'kibana/instances',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Verify monitoring settings on the instance',
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
resolvedMS: 0,
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. [View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`,
|
||||
internalShortMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`,
|
||||
action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`,
|
||||
actionPlain:
|
||||
'Verify these stack products are up and running, then double check the monitoring settings.',
|
||||
clusterName,
|
||||
count,
|
||||
stackProducts: 'Elasticsearch node: esName1, Kibana instance: kibanaInstance1',
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fire actions if under threshold', async () => {
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...missingData[0],
|
||||
gapDuration: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
const alert = new MissingMonitoringDataAlert();
|
||||
alert.initializeAlertType(
|
||||
getUiSettingsService as any,
|
||||
monitoringCluster as any,
|
||||
getLogger as any,
|
||||
config as any,
|
||||
kibanaUrl,
|
||||
false
|
||||
);
|
||||
const type = alert.getAlertType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
// @ts-ignore
|
||||
params: alert.defaultParams,
|
||||
} as any);
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: {
|
||||
clusterUuid,
|
||||
clusterName,
|
||||
},
|
||||
gapDuration: 1,
|
||||
stackProduct,
|
||||
stackProductName,
|
||||
stackProductUuid,
|
||||
ui: {
|
||||
isFiring: false,
|
||||
lastCheckedMS: 0,
|
||||
message: null,
|
||||
resolvedMS: 0,
|
||||
severity: 'danger',
|
||||
triggeredMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should resolve with a resolved message', async () => {
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...missingData[0],
|
||||
gapDuration: 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
(getState as jest.Mock).mockImplementation(() => {
|
||||
return {
|
||||
alertStates: [
|
||||
{
|
||||
cluster: {
|
||||
clusterUuid,
|
||||
clusterName,
|
||||
},
|
||||
ccs: null,
|
||||
gapDuration: 1,
|
||||
stackProduct,
|
||||
stackProductName,
|
||||
stackProductUuid,
|
||||
ui: {
|
||||
isFiring: true,
|
||||
message: null,
|
||||
severity: 'danger',
|
||||
resolvedMS: 0,
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
const alert = new MissingMonitoringDataAlert();
|
||||
alert.initializeAlertType(
|
||||
getUiSettingsService as any,
|
||||
monitoringCluster as any,
|
||||
getLogger as any,
|
||||
config as any,
|
||||
kibanaUrl,
|
||||
false
|
||||
);
|
||||
const type = alert.getAlertType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
// @ts-ignore
|
||||
params: alert.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(replaceState).toHaveBeenCalledWith({
|
||||
alertStates: [
|
||||
{
|
||||
cluster: { clusterUuid, clusterName },
|
||||
ccs: null,
|
||||
gapDuration: 1,
|
||||
stackProduct,
|
||||
stackProductName,
|
||||
stackProductUuid,
|
||||
ui: {
|
||||
isFiring: false,
|
||||
message: {
|
||||
text:
|
||||
'We are now seeing monitoring data for the Elasticsearch node: esName1, as of #resolved',
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#resolved',
|
||||
type: 'time',
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
severity: 'danger',
|
||||
resolvedMS: 1,
|
||||
triggeredMS: 1,
|
||||
lastCheckedMS: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `We are now seeing monitoring data for 1 stack product(s) in cluster testCluster.`,
|
||||
internalShortMessage: `We are now seeing monitoring data for 1 stack product(s) in cluster: testCluster.`,
|
||||
clusterName,
|
||||
count,
|
||||
stackProducts: 'Elasticsearch node: esName1',
|
||||
state: 'resolved',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const ccs = 'testCluster';
|
||||
(fetchMissingMonitoringData as jest.Mock).mockImplementation(() => {
|
||||
return [
|
||||
{
|
||||
...missingData[0],
|
||||
ccs,
|
||||
},
|
||||
];
|
||||
});
|
||||
const alert = new MissingMonitoringDataAlert();
|
||||
alert.initializeAlertType(
|
||||
getUiSettingsService as any,
|
||||
monitoringCluster as any,
|
||||
getLogger as any,
|
||||
config as any,
|
||||
kibanaUrl,
|
||||
false
|
||||
);
|
||||
const type = alert.getAlertType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
// @ts-ignore
|
||||
params: alert.defaultParams,
|
||||
} as any);
|
||||
const count = 1;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `We have not detected any monitoring data for 1 stack product(s) in cluster: testCluster. [View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123,ccs:testCluster))`,
|
||||
internalShortMessage: `We have not detected any monitoring data for 1 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`,
|
||||
action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123,ccs:testCluster))`,
|
||||
actionPlain:
|
||||
'Verify these stack products are up and running, then double check the monitoring settings.',
|
||||
clusterName,
|
||||
count,
|
||||
stackProducts: 'Elasticsearch node: esName1',
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fire with different messaging for cloud', async () => {
|
||||
const alert = new MissingMonitoringDataAlert();
|
||||
alert.initializeAlertType(
|
||||
getUiSettingsService as any,
|
||||
monitoringCluster as any,
|
||||
getLogger as any,
|
||||
config as any,
|
||||
kibanaUrl,
|
||||
true
|
||||
);
|
||||
const type = alert.getAlertType();
|
||||
await type.executor({
|
||||
...executorOptions,
|
||||
// @ts-ignore
|
||||
params: alert.defaultParams,
|
||||
} as any);
|
||||
const count = 2;
|
||||
expect(scheduleActions).toHaveBeenCalledWith('default', {
|
||||
internalFullMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`,
|
||||
internalShortMessage: `We have not detected any monitoring data for 2 stack product(s) in cluster: testCluster. Verify these stack products are up and running, then double check the monitoring settings.`,
|
||||
action: `[View what monitoring data we do have for these stack products.](http://localhost:5601/app/monitoring#overview?_g=(cluster_uuid:abc123))`,
|
||||
actionPlain:
|
||||
'Verify these stack products are up and running, then double check the monitoring settings.',
|
||||
clusterName,
|
||||
count,
|
||||
stackProducts: 'Elasticsearch node: esName1, Kibana instance: kibanaInstance1',
|
||||
state: 'firing',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,504 @@
|
|||
/*
|
||||
* 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 { IUiSettingsClient, Logger } from 'kibana/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import { BaseAlert } from './base_alert';
|
||||
import {
|
||||
AlertData,
|
||||
AlertCluster,
|
||||
AlertState,
|
||||
AlertMessage,
|
||||
AlertMissingDataState,
|
||||
AlertMissingData,
|
||||
AlertMessageTimeToken,
|
||||
AlertInstanceState,
|
||||
} from './types';
|
||||
import { AlertInstance, AlertServices } from '../../../alerts/server';
|
||||
import {
|
||||
INDEX_PATTERN,
|
||||
ALERT_MISSING_MONITORING_DATA,
|
||||
INDEX_PATTERN_ELASTICSEARCH,
|
||||
} from '../../common/constants';
|
||||
import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern';
|
||||
import { AlertMessageTokenType, AlertSeverity, AlertParamType } from '../../common/enums';
|
||||
import { RawAlertInstance } from '../../../alerts/common';
|
||||
import { parseDuration } from '../../../alerts/common/parse_duration';
|
||||
import {
|
||||
CommonAlertFilter,
|
||||
CommonAlertParams,
|
||||
CommonAlertParamDetail,
|
||||
CommonAlertStackProductFilter,
|
||||
CommonAlertNodeUuidFilter,
|
||||
} from '../../common/types';
|
||||
import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index';
|
||||
import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data';
|
||||
import { getTypeLabelForStackProduct } from '../lib/alerts/get_type_label_for_stack_product';
|
||||
import { getListingLinkForStackProduct } from '../lib/alerts/get_listing_link_for_stack_product';
|
||||
import { getStackProductLabel } from '../lib/alerts/get_stack_product_label';
|
||||
import { fetchClusters } from '../lib/alerts/fetch_clusters';
|
||||
import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs';
|
||||
import { AlertingDefaults, createLink } from './alerts_common';
|
||||
|
||||
const RESOLVED = i18n.translate('xpack.monitoring.alerts.missingData.resolved', {
|
||||
defaultMessage: 'resolved',
|
||||
});
|
||||
const FIRING = i18n.translate('xpack.monitoring.alerts.missingData.firing', {
|
||||
defaultMessage: 'firing',
|
||||
});
|
||||
|
||||
const DEFAULT_DURATION = '5m';
|
||||
const DEFAULT_LIMIT = '1d';
|
||||
|
||||
// Go a bit farther back because we need to detect the difference between seeing the monitoring data versus just not looking far enough back
|
||||
const LIMIT_BUFFER = 3 * 60 * 1000;
|
||||
|
||||
interface MissingDataParams {
|
||||
duration: string;
|
||||
limit: string;
|
||||
}
|
||||
|
||||
export class MissingMonitoringDataAlert extends BaseAlert {
|
||||
public static paramDetails = {
|
||||
duration: {
|
||||
label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', {
|
||||
defaultMessage: `Notify if monitoring data is missing for`,
|
||||
}),
|
||||
type: AlertParamType.Duration,
|
||||
} as CommonAlertParamDetail,
|
||||
limit: {
|
||||
label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.limit.label', {
|
||||
defaultMessage: `Look this far back in time for monitoring data`,
|
||||
}),
|
||||
type: AlertParamType.Duration,
|
||||
} as CommonAlertParamDetail,
|
||||
};
|
||||
|
||||
public type = ALERT_MISSING_MONITORING_DATA;
|
||||
public label = i18n.translate('xpack.monitoring.alerts.missingData.label', {
|
||||
defaultMessage: 'Missing monitoring data',
|
||||
});
|
||||
|
||||
protected defaultParams: MissingDataParams = {
|
||||
duration: DEFAULT_DURATION,
|
||||
limit: DEFAULT_LIMIT,
|
||||
};
|
||||
|
||||
protected actionVariables = [
|
||||
{
|
||||
name: 'stackProducts',
|
||||
description: i18n.translate(
|
||||
'xpack.monitoring.alerts.missingData.actionVariables.stackProducts',
|
||||
{
|
||||
defaultMessage: 'The stack products missing monitoring data.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'count',
|
||||
description: i18n.translate('xpack.monitoring.alerts.missingData.actionVariables.count', {
|
||||
defaultMessage: 'The number of stack products missing monitoring data.',
|
||||
}),
|
||||
},
|
||||
...Object.values(AlertingDefaults.ALERT_TYPE.context),
|
||||
];
|
||||
|
||||
protected async fetchClusters(
|
||||
callCluster: any,
|
||||
availableCcs: string[] | undefined = undefined,
|
||||
params: CommonAlertParams
|
||||
) {
|
||||
const limit = parseDuration(((params as unknown) as MissingDataParams).limit);
|
||||
let ccs;
|
||||
if (!availableCcs) {
|
||||
ccs = this.config.ui.ccs.enabled ? await fetchAvailableCcs(callCluster) : undefined;
|
||||
} else {
|
||||
ccs = availableCcs;
|
||||
}
|
||||
// Support CCS use cases by querying to find available remote clusters
|
||||
// and then adding those to the index pattern we are searching against
|
||||
let esIndexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN_ELASTICSEARCH);
|
||||
if (ccs) {
|
||||
esIndexPattern = getCcsIndexPattern(esIndexPattern, ccs);
|
||||
}
|
||||
return await fetchClusters(callCluster, esIndexPattern, {
|
||||
timestamp: {
|
||||
format: 'epoch_millis',
|
||||
gte: limit - LIMIT_BUFFER,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected async fetchData(
|
||||
params: CommonAlertParams,
|
||||
callCluster: any,
|
||||
clusters: AlertCluster[],
|
||||
uiSettings: IUiSettingsClient,
|
||||
availableCcs: string[]
|
||||
): Promise<AlertData[]> {
|
||||
let indexPattern = appendMetricbeatIndex(this.config, INDEX_PATTERN);
|
||||
if (availableCcs) {
|
||||
indexPattern = getCcsIndexPattern(indexPattern, availableCcs);
|
||||
}
|
||||
const duration = parseDuration(((params as unknown) as MissingDataParams).duration);
|
||||
const limit = parseDuration(((params as unknown) as MissingDataParams).limit);
|
||||
const now = +new Date();
|
||||
const missingData = await fetchMissingMonitoringData(
|
||||
callCluster,
|
||||
clusters,
|
||||
indexPattern,
|
||||
this.config.ui.max_bucket_size,
|
||||
now,
|
||||
now - limit - LIMIT_BUFFER
|
||||
);
|
||||
return missingData.map((missing) => {
|
||||
return {
|
||||
instanceKey: `${missing.clusterUuid}:${missing.stackProduct}:${missing.stackProductUuid}`,
|
||||
clusterUuid: missing.clusterUuid,
|
||||
shouldFire: missing.gapDuration > duration,
|
||||
severity: AlertSeverity.Danger,
|
||||
meta: { missing, limit },
|
||||
ccs: missing.ccs,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected filterAlertInstance(alertInstance: RawAlertInstance, filters: CommonAlertFilter[]) {
|
||||
const alertInstanceState = (alertInstance.state as unknown) as AlertInstanceState;
|
||||
if (filters && filters.length) {
|
||||
for (const filter of filters) {
|
||||
const stackProductFilter = filter as CommonAlertStackProductFilter;
|
||||
if (stackProductFilter && stackProductFilter.stackProduct) {
|
||||
let existsInState = false;
|
||||
for (const state of alertInstanceState.alertStates) {
|
||||
if ((state as AlertMissingDataState).stackProduct === stackProductFilter.stackProduct) {
|
||||
existsInState = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!existsInState) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected filterAlertState(alertState: AlertState, filters: CommonAlertFilter[]) {
|
||||
const state = alertState as AlertMissingDataState;
|
||||
if (filters && filters.length) {
|
||||
for (const filter of filters) {
|
||||
const stackProductFilter = filter as CommonAlertStackProductFilter;
|
||||
if (stackProductFilter && stackProductFilter.stackProduct) {
|
||||
if (state.stackProduct !== stackProductFilter.stackProduct) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const nodeUuidFilter = filter as CommonAlertNodeUuidFilter;
|
||||
if (nodeUuidFilter && nodeUuidFilter.nodeUuid) {
|
||||
if (state.stackProductUuid !== nodeUuidFilter.nodeUuid) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState {
|
||||
const base = super.getDefaultAlertState(cluster, item);
|
||||
return {
|
||||
...base,
|
||||
ui: {
|
||||
...base.ui,
|
||||
severity: AlertSeverity.Danger,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage {
|
||||
const { missing, limit } = item.meta as { missing: AlertMissingData; limit: number };
|
||||
if (!alertState.ui.isFiring) {
|
||||
if (missing.gapDuration > limit) {
|
||||
return {
|
||||
text: i18n.translate('xpack.monitoring.alerts.missingData.ui.notQuiteResolvedMessage', {
|
||||
defaultMessage: `We are still not seeing monitoring data for the {stackProduct} {type}: {stackProductName} and will stop trying. To change this, configure the alert to look farther back for data.`,
|
||||
values: {
|
||||
stackProduct: getStackProductLabel(missing.stackProduct),
|
||||
type: getTypeLabelForStackProduct(missing.stackProduct, false),
|
||||
stackProductName: missing.stackProductName,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: i18n.translate('xpack.monitoring.alerts.missingData.ui.resolvedMessage', {
|
||||
defaultMessage: `We are now seeing monitoring data for the {stackProduct} {type}: {stackProductName}, as of #resolved`,
|
||||
values: {
|
||||
stackProduct: getStackProductLabel(missing.stackProduct),
|
||||
type: getTypeLabelForStackProduct(missing.stackProduct, false),
|
||||
stackProductName: missing.stackProductName,
|
||||
},
|
||||
}),
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#resolved',
|
||||
type: AlertMessageTokenType.Time,
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: alertState.ui.resolvedMS,
|
||||
} as AlertMessageTimeToken,
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
text: i18n.translate('xpack.monitoring.alerts.missingData.ui.firingMessage', {
|
||||
defaultMessage: `For the past {gapDuration}, we have not detected any monitoring data from the {stackProduct} {type}: {stackProductName}, starting at #absolute`,
|
||||
values: {
|
||||
gapDuration: moment.duration(missing.gapDuration, 'milliseconds').humanize(),
|
||||
stackProduct: getStackProductLabel(missing.stackProduct),
|
||||
type: getTypeLabelForStackProduct(missing.stackProduct, false),
|
||||
stackProductName: missing.stackProductName,
|
||||
},
|
||||
}),
|
||||
nextSteps: [
|
||||
createLink(
|
||||
i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.viewAll', {
|
||||
defaultMessage: `#start_linkView all {stackProduct} {type}#end_link`,
|
||||
values: {
|
||||
type: getTypeLabelForStackProduct(missing.stackProduct),
|
||||
stackProduct: getStackProductLabel(missing.stackProduct),
|
||||
},
|
||||
}),
|
||||
getListingLinkForStackProduct(missing.stackProduct),
|
||||
AlertMessageTokenType.Link
|
||||
),
|
||||
{
|
||||
text: i18n.translate('xpack.monitoring.alerts.missingData.ui.nextSteps.verifySettings', {
|
||||
defaultMessage: `Verify monitoring settings on the {type}`,
|
||||
values: {
|
||||
type: getTypeLabelForStackProduct(missing.stackProduct, false),
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
tokens: [
|
||||
{
|
||||
startToken: '#absolute',
|
||||
type: AlertMessageTokenType.Time,
|
||||
isAbsolute: true,
|
||||
isRelative: false,
|
||||
timestamp: alertState.ui.triggeredMS,
|
||||
} as AlertMessageTimeToken,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
protected executeActions(
|
||||
instance: AlertInstance,
|
||||
instanceState: AlertInstanceState,
|
||||
item: AlertData | null,
|
||||
cluster: AlertCluster
|
||||
) {
|
||||
if (instanceState.alertStates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ccs = instanceState.alertStates.reduce((accum: string, state): string => {
|
||||
if (state.ccs) {
|
||||
return state.ccs;
|
||||
}
|
||||
return accum;
|
||||
}, '');
|
||||
|
||||
const firingCount = instanceState.alertStates.filter((alertState) => alertState.ui.isFiring)
|
||||
.length;
|
||||
const firingStackProducts = instanceState.alertStates
|
||||
.filter((_state) => (_state as AlertMissingDataState).ui.isFiring)
|
||||
.map((_state) => {
|
||||
const state = _state as AlertMissingDataState;
|
||||
return `${getStackProductLabel(state.stackProduct)} ${getTypeLabelForStackProduct(
|
||||
state.stackProduct,
|
||||
false
|
||||
)}: ${state.stackProductName}`;
|
||||
})
|
||||
.join(', ');
|
||||
if (firingCount > 0) {
|
||||
const shortActionText = i18n.translate('xpack.monitoring.alerts.missingData.shortAction', {
|
||||
defaultMessage:
|
||||
'Verify these stack products are up and running, then double check the monitoring settings.',
|
||||
});
|
||||
const fullActionText = i18n.translate('xpack.monitoring.alerts.missingData.fullAction', {
|
||||
defaultMessage: 'View what monitoring data we do have for these stack products.',
|
||||
});
|
||||
const globalState = [`cluster_uuid:${cluster.clusterUuid}`];
|
||||
if (ccs) {
|
||||
globalState.push(`ccs:${ccs}`);
|
||||
}
|
||||
const url = `${this.kibanaUrl}/app/monitoring#overview?_g=(${globalState.join(',')})`;
|
||||
const action = `[${fullActionText}](${url})`;
|
||||
const internalShortMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.missingData.firing.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `We have not detected any monitoring data for {count} stack product(s) in cluster: {clusterName}. {shortActionText}`,
|
||||
values: {
|
||||
count: firingCount,
|
||||
clusterName: cluster.clusterName,
|
||||
shortActionText,
|
||||
},
|
||||
}
|
||||
);
|
||||
const internalFullMessage = i18n.translate(
|
||||
'xpack.monitoring.alerts.missingData.firing.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `We have not detected any monitoring data for {count} stack product(s) in cluster: {clusterName}. {action}`,
|
||||
values: {
|
||||
count: firingCount,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
},
|
||||
}
|
||||
);
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage,
|
||||
internalFullMessage: this.isCloud ? internalShortMessage : internalFullMessage,
|
||||
state: FIRING,
|
||||
stackProducts: firingStackProducts,
|
||||
count: firingCount,
|
||||
clusterName: cluster.clusterName,
|
||||
action,
|
||||
actionPlain: shortActionText,
|
||||
});
|
||||
} else {
|
||||
const resolvedCount = instanceState.alertStates.filter(
|
||||
(alertState) => !alertState.ui.isFiring
|
||||
).length;
|
||||
const resolvedStackProducts = instanceState.alertStates
|
||||
.filter((_state) => !(_state as AlertMissingDataState).ui.isFiring)
|
||||
.map((_state) => {
|
||||
const state = _state as AlertMissingDataState;
|
||||
return `${getStackProductLabel(state.stackProduct)} ${getTypeLabelForStackProduct(
|
||||
state.stackProduct,
|
||||
false
|
||||
)}: ${state.stackProductName}`;
|
||||
})
|
||||
.join(',');
|
||||
if (resolvedCount > 0) {
|
||||
instance.scheduleActions('default', {
|
||||
internalShortMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.missingData.resolved.internalShortMessage',
|
||||
{
|
||||
defaultMessage: `We are now seeing monitoring data for {count} stack product(s) in cluster: {clusterName}.`,
|
||||
values: {
|
||||
count: resolvedCount,
|
||||
clusterName: cluster.clusterName,
|
||||
},
|
||||
}
|
||||
),
|
||||
internalFullMessage: i18n.translate(
|
||||
'xpack.monitoring.alerts.missingData.resolved.internalFullMessage',
|
||||
{
|
||||
defaultMessage: `We are now seeing monitoring data for {count} stack product(s) in cluster {clusterName}.`,
|
||||
values: {
|
||||
count: resolvedCount,
|
||||
clusterName: cluster.clusterName,
|
||||
},
|
||||
}
|
||||
),
|
||||
state: RESOLVED,
|
||||
stackProducts: resolvedStackProducts,
|
||||
count: resolvedCount,
|
||||
clusterName: cluster.clusterName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async processData(
|
||||
data: AlertData[],
|
||||
clusters: AlertCluster[],
|
||||
services: AlertServices,
|
||||
logger: Logger
|
||||
) {
|
||||
for (const cluster of clusters) {
|
||||
const stackProducts = data.filter((_item) => _item.clusterUuid === cluster.clusterUuid);
|
||||
if (stackProducts.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firingInstances = stackProducts.reduce((list: string[], stackProduct) => {
|
||||
const { missing } = stackProduct.meta as { missing: AlertMissingData; limit: number };
|
||||
if (stackProduct.shouldFire) {
|
||||
list.push(`${missing.stackProduct}:${missing.stackProductUuid}`);
|
||||
}
|
||||
return list;
|
||||
}, [] as string[]);
|
||||
firingInstances.sort(); // It doesn't matter how we sort, but keep the order consistent
|
||||
const instanceId = `${this.type}:${cluster.clusterUuid}:${firingInstances.join(',')}`;
|
||||
const instance = services.alertInstanceFactory(instanceId);
|
||||
const instanceState = (instance.getState() as unknown) as AlertInstanceState;
|
||||
const alertInstanceState: AlertInstanceState = {
|
||||
alertStates: instanceState?.alertStates || [],
|
||||
};
|
||||
let shouldExecuteActions = false;
|
||||
for (const stackProduct of stackProducts) {
|
||||
const { missing } = stackProduct.meta as { missing: AlertMissingData; limit: number };
|
||||
let state: AlertMissingDataState;
|
||||
const indexInState = alertInstanceState.alertStates.findIndex((alertState) => {
|
||||
const _alertState = alertState as AlertMissingDataState;
|
||||
return (
|
||||
_alertState.cluster.clusterUuid === cluster.clusterUuid &&
|
||||
_alertState.stackProduct === missing.stackProduct &&
|
||||
_alertState.stackProductUuid === missing.stackProductUuid
|
||||
);
|
||||
});
|
||||
if (indexInState > -1) {
|
||||
state = alertInstanceState.alertStates[indexInState] as AlertMissingDataState;
|
||||
} else {
|
||||
state = this.getDefaultAlertState(cluster, stackProduct) as AlertMissingDataState;
|
||||
}
|
||||
|
||||
state.stackProduct = missing.stackProduct;
|
||||
state.stackProductUuid = missing.stackProductUuid;
|
||||
state.stackProductName = missing.stackProductName;
|
||||
state.gapDuration = missing.gapDuration;
|
||||
|
||||
if (stackProduct.shouldFire) {
|
||||
if (!state.ui.isFiring) {
|
||||
state.ui.triggeredMS = new Date().valueOf();
|
||||
}
|
||||
state.ui.isFiring = true;
|
||||
state.ui.message = this.getUiMessage(state, stackProduct);
|
||||
state.ui.severity = stackProduct.severity;
|
||||
state.ui.resolvedMS = 0;
|
||||
shouldExecuteActions = true;
|
||||
} else if (!stackProduct.shouldFire && state.ui.isFiring) {
|
||||
state.ui.isFiring = false;
|
||||
state.ui.resolvedMS = new Date().valueOf();
|
||||
state.ui.message = this.getUiMessage(state, stackProduct);
|
||||
shouldExecuteActions = true;
|
||||
}
|
||||
|
||||
if (indexInState === -1) {
|
||||
alertInstanceState.alertStates.push(state);
|
||||
} else {
|
||||
alertInstanceState.alertStates = [
|
||||
...alertInstanceState.alertStates.slice(0, indexInState),
|
||||
state,
|
||||
...alertInstanceState.alertStates.slice(indexInState + 1),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
instance.replaceState(alertInstanceState);
|
||||
if (shouldExecuteActions) {
|
||||
this.executeActions(instance, alertInstanceState, null, cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,13 @@ export interface AlertCpuUsageState extends AlertState {
|
|||
nodeName: string;
|
||||
}
|
||||
|
||||
export interface AlertMissingDataState extends AlertState {
|
||||
stackProduct: string;
|
||||
stackProductUuid: string;
|
||||
stackProductName: string;
|
||||
gapDuration: number;
|
||||
}
|
||||
|
||||
export interface AlertDiskUsageState extends AlertState {
|
||||
diskUsage: number;
|
||||
nodeId: string;
|
||||
|
@ -93,6 +100,15 @@ export interface AlertDiskUsageNodeStats {
|
|||
ccs?: string;
|
||||
}
|
||||
|
||||
export interface AlertMissingData {
|
||||
stackProduct: string;
|
||||
stackProductUuid: string;
|
||||
stackProductName: string;
|
||||
clusterUuid: string;
|
||||
gapDuration: number;
|
||||
ccs?: string;
|
||||
}
|
||||
|
||||
export interface AlertData {
|
||||
instanceKey: string;
|
||||
clusterUuid: string;
|
||||
|
|
|
@ -6,7 +6,18 @@
|
|||
import { get } from 'lodash';
|
||||
import { AlertCluster } from '../../alerts/types';
|
||||
|
||||
export async function fetchClusters(callCluster: any, index: string): Promise<AlertCluster[]> {
|
||||
interface RangeFilter {
|
||||
[field: string]: {
|
||||
format?: string;
|
||||
gte: string | number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchClusters(
|
||||
callCluster: any,
|
||||
index: string,
|
||||
rangeFilter: RangeFilter = { timestamp: { gte: 'now-2m' } }
|
||||
): Promise<AlertCluster[]> {
|
||||
const params = {
|
||||
index,
|
||||
filterPath: [
|
||||
|
@ -25,11 +36,7 @@ export async function fetchClusters(callCluster: any, index: string): Promise<Al
|
|||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
timestamp: {
|
||||
gte: 'now-2m',
|
||||
},
|
||||
},
|
||||
range: rangeFilter,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* 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 { fetchMissingMonitoringData } from './fetch_missing_monitoring_data';
|
||||
|
||||
function getResponse(
|
||||
index: string,
|
||||
products: Array<{
|
||||
uuid: string;
|
||||
timestamp: number;
|
||||
nameSource: any;
|
||||
}>
|
||||
) {
|
||||
return {
|
||||
buckets: products.map((product) => {
|
||||
return {
|
||||
key: product.uuid,
|
||||
most_recent: {
|
||||
value: product.timestamp,
|
||||
},
|
||||
document: {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_index: index,
|
||||
_source: product.nameSource,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('fetchMissingMonitoringData', () => {
|
||||
let callCluster = jest.fn();
|
||||
const index = '.monitoring-*';
|
||||
const startMs = 100;
|
||||
const size = 10;
|
||||
|
||||
it('fetch as expected', async () => {
|
||||
const now = 10;
|
||||
const clusters = [
|
||||
{
|
||||
clusterUuid: 'clusterUuid1',
|
||||
clusterName: 'clusterName1',
|
||||
},
|
||||
];
|
||||
callCluster = jest.fn().mockImplementation((...args) => {
|
||||
return {
|
||||
aggregations: {
|
||||
clusters: {
|
||||
buckets: clusters.map((cluster) => ({
|
||||
key: cluster.clusterUuid,
|
||||
es_uuids: getResponse('.monitoring-es-*', [
|
||||
{
|
||||
uuid: 'nodeUuid1',
|
||||
nameSource: {
|
||||
source_node: {
|
||||
name: 'nodeName1',
|
||||
},
|
||||
},
|
||||
timestamp: 9,
|
||||
},
|
||||
{
|
||||
uuid: 'nodeUuid2',
|
||||
nameSource: {
|
||||
source_node: {
|
||||
name: 'nodeName2',
|
||||
},
|
||||
},
|
||||
timestamp: 2,
|
||||
},
|
||||
]),
|
||||
kibana_uuids: getResponse('.monitoring-kibana-*', [
|
||||
{
|
||||
uuid: 'kibanaUuid1',
|
||||
nameSource: {
|
||||
kibana_stats: {
|
||||
kibana: {
|
||||
name: 'kibanaName1',
|
||||
},
|
||||
},
|
||||
},
|
||||
timestamp: 4,
|
||||
},
|
||||
]),
|
||||
logstash_uuids: getResponse('.monitoring-logstash-*', [
|
||||
{
|
||||
uuid: 'logstashUuid1',
|
||||
nameSource: {
|
||||
logstash_stats: {
|
||||
logstash: {
|
||||
host: 'logstashName1',
|
||||
},
|
||||
},
|
||||
},
|
||||
timestamp: 2,
|
||||
},
|
||||
]),
|
||||
beats: {
|
||||
beats_uuids: getResponse('.monitoring-beats-*', [
|
||||
{
|
||||
uuid: 'beatUuid1',
|
||||
nameSource: {
|
||||
beats_stats: {
|
||||
beat: {
|
||||
name: 'beatName1',
|
||||
},
|
||||
},
|
||||
},
|
||||
timestamp: 0,
|
||||
},
|
||||
]),
|
||||
},
|
||||
apms: {
|
||||
apm_uuids: getResponse('.monitoring-beats-*', [
|
||||
{
|
||||
uuid: 'apmUuid1',
|
||||
nameSource: {
|
||||
beats_stats: {
|
||||
beat: {
|
||||
name: 'apmName1',
|
||||
type: 'apm-server',
|
||||
},
|
||||
},
|
||||
},
|
||||
timestamp: 1,
|
||||
},
|
||||
]),
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const result = await fetchMissingMonitoringData(
|
||||
callCluster,
|
||||
clusters,
|
||||
index,
|
||||
size,
|
||||
now,
|
||||
startMs
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
stackProduct: 'elasticsearch',
|
||||
stackProductUuid: 'nodeUuid1',
|
||||
stackProductName: 'nodeName1',
|
||||
clusterUuid: 'clusterUuid1',
|
||||
gapDuration: 1,
|
||||
ccs: null,
|
||||
},
|
||||
{
|
||||
stackProduct: 'elasticsearch',
|
||||
stackProductUuid: 'nodeUuid2',
|
||||
stackProductName: 'nodeName2',
|
||||
clusterUuid: 'clusterUuid1',
|
||||
gapDuration: 8,
|
||||
ccs: null,
|
||||
},
|
||||
{
|
||||
stackProduct: 'kibana',
|
||||
stackProductUuid: 'kibanaUuid1',
|
||||
stackProductName: 'kibanaName1',
|
||||
clusterUuid: 'clusterUuid1',
|
||||
gapDuration: 6,
|
||||
ccs: null,
|
||||
},
|
||||
{
|
||||
stackProduct: 'logstash',
|
||||
stackProductUuid: 'logstashUuid1',
|
||||
stackProductName: 'logstashName1',
|
||||
clusterUuid: 'clusterUuid1',
|
||||
gapDuration: 8,
|
||||
ccs: null,
|
||||
},
|
||||
{
|
||||
stackProduct: 'beats',
|
||||
stackProductUuid: 'beatUuid1',
|
||||
stackProductName: 'beatName1',
|
||||
clusterUuid: 'clusterUuid1',
|
||||
gapDuration: 10,
|
||||
ccs: null,
|
||||
},
|
||||
{
|
||||
stackProduct: 'apm',
|
||||
stackProductUuid: 'apmUuid1',
|
||||
stackProductName: 'apmName1',
|
||||
clusterUuid: 'clusterUuid1',
|
||||
gapDuration: 9,
|
||||
ccs: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle ccs', async () => {
|
||||
const now = 10;
|
||||
const clusters = [
|
||||
{
|
||||
clusterUuid: 'clusterUuid1',
|
||||
clusterName: 'clusterName1',
|
||||
},
|
||||
];
|
||||
callCluster = jest.fn().mockImplementation((...args) => {
|
||||
return {
|
||||
aggregations: {
|
||||
clusters: {
|
||||
buckets: clusters.map((cluster) => ({
|
||||
key: cluster.clusterUuid,
|
||||
es_uuids: getResponse('Monitoring:.monitoring-es-*', [
|
||||
{
|
||||
uuid: 'nodeUuid1',
|
||||
nameSource: {
|
||||
source_node: {
|
||||
name: 'nodeName1',
|
||||
},
|
||||
},
|
||||
timestamp: 9,
|
||||
},
|
||||
]),
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const result = await fetchMissingMonitoringData(
|
||||
callCluster,
|
||||
clusters,
|
||||
index,
|
||||
size,
|
||||
now,
|
||||
startMs
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
stackProduct: 'elasticsearch',
|
||||
stackProductUuid: 'nodeUuid1',
|
||||
stackProductName: 'nodeName1',
|
||||
clusterUuid: 'clusterUuid1',
|
||||
gapDuration: 1,
|
||||
ccs: 'Monitoring',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { get } from 'lodash';
|
||||
import { AlertCluster, AlertMissingData } from '../../alerts/types';
|
||||
import {
|
||||
KIBANA_SYSTEM_ID,
|
||||
BEATS_SYSTEM_ID,
|
||||
APM_SYSTEM_ID,
|
||||
LOGSTASH_SYSTEM_ID,
|
||||
ELASTICSEARCH_SYSTEM_ID,
|
||||
} from '../../../common/constants';
|
||||
|
||||
interface ClusterBucketESResponse {
|
||||
key: string;
|
||||
kibana_uuids?: UuidResponse;
|
||||
logstash_uuids?: UuidResponse;
|
||||
es_uuids?: UuidResponse;
|
||||
beats?: {
|
||||
beats_uuids: UuidResponse;
|
||||
};
|
||||
apms?: {
|
||||
apm_uuids: UuidResponse;
|
||||
};
|
||||
}
|
||||
|
||||
interface UuidResponse {
|
||||
buckets: UuidBucketESResponse[];
|
||||
}
|
||||
|
||||
interface UuidBucketESResponse {
|
||||
key: string;
|
||||
most_recent: {
|
||||
value: number;
|
||||
};
|
||||
document: {
|
||||
hits: {
|
||||
hits: TopHitESResponse[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface TopHitESResponse {
|
||||
_index: string;
|
||||
_source: {
|
||||
source_node?: {
|
||||
name: string;
|
||||
};
|
||||
kibana_stats?: {
|
||||
kibana: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
logstash_stats?: {
|
||||
logstash: {
|
||||
host: string;
|
||||
};
|
||||
};
|
||||
beats_stats?: {
|
||||
beat: {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function getStackProductFromIndex(index: string, beatType: string) {
|
||||
if (index.includes('-kibana-')) {
|
||||
return KIBANA_SYSTEM_ID;
|
||||
}
|
||||
if (index.includes('-beats-')) {
|
||||
if (beatType === 'apm-server') {
|
||||
return APM_SYSTEM_ID;
|
||||
}
|
||||
return BEATS_SYSTEM_ID;
|
||||
}
|
||||
if (index.includes('-logstash-')) {
|
||||
return LOGSTASH_SYSTEM_ID;
|
||||
}
|
||||
if (index.includes('-es-')) {
|
||||
return ELASTICSEARCH_SYSTEM_ID;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function fetchMissingMonitoringData(
|
||||
callCluster: any,
|
||||
clusters: AlertCluster[],
|
||||
index: string,
|
||||
size: number,
|
||||
nowInMs: number,
|
||||
startMs: number
|
||||
): Promise<AlertMissingData[]> {
|
||||
const endMs = nowInMs;
|
||||
|
||||
const nameFields = [
|
||||
'source_node.name',
|
||||
'kibana_stats.kibana.name',
|
||||
'logstash_stats.logstash.host',
|
||||
'beats_stats.beat.name',
|
||||
'beat_stats.beat.type',
|
||||
];
|
||||
const subAggs = {
|
||||
most_recent: {
|
||||
max: {
|
||||
field: 'timestamp',
|
||||
},
|
||||
},
|
||||
document: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
sort: [
|
||||
{
|
||||
timestamp: {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
_source: {
|
||||
includes: ['_index', ...nameFields],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const params = {
|
||||
index,
|
||||
filterPath: ['aggregations.clusters.buckets'],
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
terms: {
|
||||
cluster_uuid: clusters.map((cluster) => cluster.clusterUuid),
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
timestamp: {
|
||||
format: 'epoch_millis',
|
||||
gte: startMs,
|
||||
lte: endMs,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
clusters: {
|
||||
terms: {
|
||||
field: 'cluster_uuid',
|
||||
size,
|
||||
},
|
||||
aggs: {
|
||||
es_uuids: {
|
||||
terms: {
|
||||
field: 'node_stats.node_id',
|
||||
size,
|
||||
},
|
||||
aggs: subAggs,
|
||||
},
|
||||
kibana_uuids: {
|
||||
terms: {
|
||||
field: 'kibana_stats.kibana.uuid',
|
||||
size,
|
||||
},
|
||||
aggs: subAggs,
|
||||
},
|
||||
beats: {
|
||||
filter: {
|
||||
bool: {
|
||||
must_not: {
|
||||
term: {
|
||||
'beats_stats.beat.type': 'apm-server',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
beats_uuids: {
|
||||
terms: {
|
||||
field: 'beats_stats.beat.uuid',
|
||||
size,
|
||||
},
|
||||
aggs: subAggs,
|
||||
},
|
||||
},
|
||||
},
|
||||
apms: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: {
|
||||
term: {
|
||||
'beats_stats.beat.type': 'apm-server',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
apm_uuids: {
|
||||
terms: {
|
||||
field: 'beats_stats.beat.uuid',
|
||||
size,
|
||||
},
|
||||
aggs: subAggs,
|
||||
},
|
||||
},
|
||||
},
|
||||
logstash_uuids: {
|
||||
terms: {
|
||||
field: 'logstash_stats.logstash.uuid',
|
||||
size,
|
||||
},
|
||||
aggs: subAggs,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await callCluster('search', params);
|
||||
const clusterBuckets = get(
|
||||
response,
|
||||
'aggregations.clusters.buckets',
|
||||
[]
|
||||
) as ClusterBucketESResponse[];
|
||||
const uniqueList: { [id: string]: AlertMissingData } = {};
|
||||
for (const clusterBucket of clusterBuckets) {
|
||||
const clusterUuid = clusterBucket.key;
|
||||
|
||||
const uuidBuckets = [
|
||||
...(clusterBucket.es_uuids?.buckets || []),
|
||||
...(clusterBucket.kibana_uuids?.buckets || []),
|
||||
...(clusterBucket.logstash_uuids?.buckets || []),
|
||||
...(clusterBucket.beats?.beats_uuids.buckets || []),
|
||||
...(clusterBucket.apms?.apm_uuids.buckets || []),
|
||||
];
|
||||
|
||||
for (const uuidBucket of uuidBuckets) {
|
||||
const stackProductUuid = uuidBucket.key;
|
||||
const indexName = get(uuidBucket, `document.hits.hits[0]._index`);
|
||||
const stackProduct = getStackProductFromIndex(
|
||||
indexName,
|
||||
get(uuidBucket, `document.hits.hits[0]._source.beats_stats.beat.type`)
|
||||
);
|
||||
const differenceInMs = nowInMs - uuidBucket.most_recent.value;
|
||||
let stackProductName = stackProductUuid;
|
||||
for (const nameField of nameFields) {
|
||||
stackProductName = get(uuidBucket, `document.hits.hits[0]._source.${nameField}`);
|
||||
if (stackProductName) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uniqueList[`${clusterUuid}${stackProduct}${stackProductUuid}`] = {
|
||||
stackProduct,
|
||||
stackProductUuid,
|
||||
stackProductName,
|
||||
clusterUuid,
|
||||
gapDuration: differenceInMs,
|
||||
ccs: indexName.includes(':') ? indexName.split(':')[0] : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const missingData = Object.values(uniqueList);
|
||||
return missingData;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 {
|
||||
BEATS_SYSTEM_ID,
|
||||
ELASTICSEARCH_SYSTEM_ID,
|
||||
KIBANA_SYSTEM_ID,
|
||||
LOGSTASH_SYSTEM_ID,
|
||||
APM_SYSTEM_ID,
|
||||
} from '../../../common/constants';
|
||||
|
||||
export function getListingLinkForStackProduct(stackProduct: string) {
|
||||
switch (stackProduct) {
|
||||
case ELASTICSEARCH_SYSTEM_ID:
|
||||
return 'elasticsearch/nodes';
|
||||
case LOGSTASH_SYSTEM_ID:
|
||||
return 'logstash/nodes';
|
||||
case KIBANA_SYSTEM_ID:
|
||||
return 'kibana/instances';
|
||||
case BEATS_SYSTEM_ID:
|
||||
return 'beats/beats';
|
||||
case APM_SYSTEM_ID:
|
||||
return 'apm/instances';
|
||||
}
|
||||
return '';
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { capitalize } from 'lodash';
|
||||
import { APM_SYSTEM_ID, BEATS_SYSTEM_ID } from '../../../common/constants';
|
||||
|
||||
export function getStackProductLabel(stackProduct: string) {
|
||||
switch (stackProduct) {
|
||||
case APM_SYSTEM_ID:
|
||||
return 'APM';
|
||||
case BEATS_SYSTEM_ID:
|
||||
return 'Beat';
|
||||
}
|
||||
return capitalize(stackProduct);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 {
|
||||
BEATS_SYSTEM_ID,
|
||||
ELASTICSEARCH_SYSTEM_ID,
|
||||
KIBANA_SYSTEM_ID,
|
||||
LOGSTASH_SYSTEM_ID,
|
||||
APM_SYSTEM_ID,
|
||||
} from '../../../common/constants';
|
||||
|
||||
const NODES = i18n.translate('xpack.monitoring.alerts.typeLabel.nodes', {
|
||||
defaultMessage: 'nodes',
|
||||
});
|
||||
|
||||
const INSTANCES = i18n.translate('xpack.monitoring.alerts.typeLabel.instances', {
|
||||
defaultMessage: 'instances',
|
||||
});
|
||||
|
||||
const SERVERS = i18n.translate('xpack.monitoring.alerts.typeLabel.servers', {
|
||||
defaultMessage: 'servers',
|
||||
});
|
||||
|
||||
const NODE = i18n.translate('xpack.monitoring.alerts.typeLabel.node', {
|
||||
defaultMessage: 'node',
|
||||
});
|
||||
|
||||
const INSTANCE = i18n.translate('xpack.monitoring.alerts.typeLabel.instance', {
|
||||
defaultMessage: 'instance',
|
||||
});
|
||||
|
||||
const SERVER = i18n.translate('xpack.monitoring.alerts.typeLabel.server', {
|
||||
defaultMessage: 'server',
|
||||
});
|
||||
|
||||
export function getTypeLabelForStackProduct(stackProduct: string, plural: boolean = true) {
|
||||
switch (stackProduct) {
|
||||
case ELASTICSEARCH_SYSTEM_ID:
|
||||
case LOGSTASH_SYSTEM_ID:
|
||||
return plural ? NODES : NODE;
|
||||
case KIBANA_SYSTEM_ID:
|
||||
case BEATS_SYSTEM_ID:
|
||||
return plural ? INSTANCES : INSTANCE;
|
||||
case APM_SYSTEM_ID:
|
||||
return plural ? SERVERS : SERVER;
|
||||
}
|
||||
return 'n/a';
|
||||
}
|
Loading…
Reference in a new issue