[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:
Chris Roberson 2020-10-01 12:28:34 -04:00 committed by GitHub
parent 198c5d9988
commit a61f4d4cbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 2303 additions and 89 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { 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;
}

View file

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

View file

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

View file

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

View file

@ -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"
/>

View file

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

View file

@ -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"
/>

View file

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

View file

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

View file

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

View file

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

View file

@ -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" />;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) => (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) => (

View file

@ -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) => (

View file

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

View file

@ -63,6 +63,6 @@ describe('AlertsFactory', () => {
it('should get all', () => {
const alerts = AlertsFactory.getAll();
expect(alerts.length).toBe(8);
expect(alerts.length).toBe(9);
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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