Skip to content

Commit

Permalink
[Monitoring] Missing data alert (#78208)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
chrisronline authored Oct 1, 2020
1 parent 198c5d9 commit a61f4d4
Show file tree
Hide file tree
Showing 59 changed files with 2,303 additions and 89 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/monitoring/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -249,6 +250,7 @@ export const ALERTS = [
ALERT_ELASTICSEARCH_VERSION_MISMATCH,
ALERT_KIBANA_VERSION_MISMATCH,
ALERT_LOGSTASH_VERSION_MISMATCH,
ALERT_MISSING_MONITORING_DATA,
];

/**
Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/monitoring/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 11 additions & 4 deletions x-pack/plugins/monitoring/public/alerts/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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} />,
};
}),
];
Expand Down Expand Up @@ -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}
/>
),
};
}),
];
Expand Down
11 changes: 6 additions & 5 deletions x-pack/plugins/monitoring/public/alerts/callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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;

Expand Down
23 changes: 23 additions & 0 deletions x-pack/plugins/monitoring/public/alerts/filter_alert_states.ts
Original file line number Diff line number Diff line change
@@ -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;
},
{}
);
}
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 7 additions & 3 deletions x-pack/plugins/monitoring/public/alerts/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down
15 changes: 12 additions & 3 deletions x-pack/plugins/monitoring/public/alerts/status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -78,6 +78,7 @@ export function Status({ stats }) {
return (
<SummaryStatus
metrics={metrics}
alerts={alerts}
IconComponent={IconComponent}
data-test-subj="apmDetailStatus"
/>
Expand Down
Loading

0 comments on commit a61f4d4

Please sign in to comment.