Skip to content

Commit

Permalink
[7.x] [Metrics UI] Add Metrics Anomaly Alert Type (#89244) (#90893)
Browse files Browse the repository at this point in the history
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
Zacqary and kibanamachine committed Feb 10, 2021
1 parent f05a718 commit fdd759d
Show file tree
Hide file tree
Showing 50 changed files with 1,918 additions and 480 deletions.
43 changes: 41 additions & 2 deletions x-pack/plugins/infra/common/alerting/metrics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import * as rt from 'io-ts';
import { ANOMALY_THRESHOLD } from '../../infra_ml';
import { ItemTypeRT } from '../../inventory_models/types';

// TODO: Have threshold and inventory alerts import these types from this file instead of from their
// local directories
export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
export const METRIC_ANOMALY_ALERT_TYPE_ID = 'metrics.alert.anomaly';

export enum Comparator {
GT = '>',
Expand All @@ -34,6 +35,26 @@ export enum Aggregators {
P99 = 'p99',
}

const metricAnomalyNodeTypeRT = rt.union([rt.literal('hosts'), rt.literal('k8s')]);
const metricAnomalyMetricRT = rt.union([
rt.literal('memory_usage'),
rt.literal('network_in'),
rt.literal('network_out'),
]);
const metricAnomalyInfluencerFilterRT = rt.type({
fieldName: rt.string,
fieldValue: rt.string,
});

export interface MetricAnomalyParams {
nodeType: rt.TypeOf<typeof metricAnomalyNodeTypeRT>;
metric: rt.TypeOf<typeof metricAnomalyMetricRT>;
alertInterval?: string;
sourceId?: string;
threshold: Exclude<ANOMALY_THRESHOLD, ANOMALY_THRESHOLD.LOW>;
influencerFilter: rt.TypeOf<typeof metricAnomalyInfluencerFilterRT> | undefined;
}

// Alert Preview API
const baseAlertRequestParamsRT = rt.intersection([
rt.partial({
Expand All @@ -51,7 +72,6 @@ const baseAlertRequestParamsRT = rt.intersection([
rt.literal('M'),
rt.literal('y'),
]),
criteria: rt.array(rt.any),
alertInterval: rt.string,
alertThrottle: rt.string,
alertOnNoData: rt.boolean,
Expand All @@ -65,6 +85,7 @@ const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([
}),
rt.type({
alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID),
criteria: rt.array(rt.any),
}),
]);
export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf<
Expand All @@ -76,15 +97,33 @@ const inventoryAlertPreviewRequestParamsRT = rt.intersection([
rt.type({
nodeType: ItemTypeRT,
alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID),
criteria: rt.array(rt.any),
}),
]);
export type InventoryAlertPreviewRequestParams = rt.TypeOf<
typeof inventoryAlertPreviewRequestParamsRT
>;

const metricAnomalyAlertPreviewRequestParamsRT = rt.intersection([
baseAlertRequestParamsRT,
rt.type({
nodeType: metricAnomalyNodeTypeRT,
metric: metricAnomalyMetricRT,
threshold: rt.number,
alertType: rt.literal(METRIC_ANOMALY_ALERT_TYPE_ID),
}),
rt.partial({
influencerFilter: metricAnomalyInfluencerFilterRT,
}),
]);
export type MetricAnomalyAlertPreviewRequestParams = rt.TypeOf<
typeof metricAnomalyAlertPreviewRequestParamsRT
>;

export const alertPreviewRequestParamsRT = rt.union([
metricThresholdAlertPreviewRequestParamsRT,
inventoryAlertPreviewRequestParamsRT,
metricAnomalyAlertPreviewRequestParamsRT,
]);
export type AlertPreviewRequestParams = rt.TypeOf<typeof alertPreviewRequestParamsRT>;

Expand Down
56 changes: 32 additions & 24 deletions x-pack/plugins/infra/common/infra_ml/anomaly_results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,44 @@
* 2.0.
*/

export const ML_SEVERITY_SCORES = {
warning: 3,
minor: 25,
major: 50,
critical: 75,
};
export enum ANOMALY_SEVERITY {
CRITICAL = 'critical',
MAJOR = 'major',
MINOR = 'minor',
WARNING = 'warning',
LOW = 'low',
UNKNOWN = 'unknown',
}

export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES;
export enum ANOMALY_THRESHOLD {
CRITICAL = 75,
MAJOR = 50,
MINOR = 25,
WARNING = 3,
LOW = 0,
}

export const ML_SEVERITY_COLORS = {
critical: 'rgb(228, 72, 72)',
major: 'rgb(229, 113, 0)',
minor: 'rgb(255, 221, 0)',
warning: 'rgb(125, 180, 226)',
export const SEVERITY_COLORS = {
CRITICAL: '#fe5050',
MAJOR: '#fba740',
MINOR: '#fdec25',
WARNING: '#8bc8fb',
LOW: '#d2e9f7',
BLANK: '#ffffff',
};

export const getSeverityCategoryForScore = (
score: number
): MLSeverityScoreCategories | undefined => {
if (score >= ML_SEVERITY_SCORES.critical) {
return 'critical';
} else if (score >= ML_SEVERITY_SCORES.major) {
return 'major';
} else if (score >= ML_SEVERITY_SCORES.minor) {
return 'minor';
} else if (score >= ML_SEVERITY_SCORES.warning) {
return 'warning';
export const getSeverityCategoryForScore = (score: number): ANOMALY_SEVERITY | undefined => {
if (score >= ANOMALY_THRESHOLD.CRITICAL) {
return ANOMALY_SEVERITY.CRITICAL;
} else if (score >= ANOMALY_THRESHOLD.MAJOR) {
return ANOMALY_SEVERITY.MAJOR;
} else if (score >= ANOMALY_THRESHOLD.MINOR) {
return ANOMALY_SEVERITY.MINOR;
} else if (score >= ANOMALY_THRESHOLD.WARNING) {
return ANOMALY_SEVERITY.WARNING;
} else {
// Category is too low to include
return undefined;
return ANOMALY_SEVERITY.LOW;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface Props {
alertInterval: string;
alertThrottle: string;
alertType: PreviewableAlertTypes;
alertParams: { criteria: any[]; sourceId: string } & Record<string, any>;
alertParams: { criteria?: any[]; sourceId: string } & Record<string, any>;
validate: (params: any) => ValidationResult;
showNoDataResults?: boolean;
groupByDisplayName?: string;
Expand Down Expand Up @@ -109,6 +109,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
}, [previewLookbackInterval, alertInterval]);

const isPreviewDisabled = useMemo(() => {
if (!alertParams.criteria) return false;
const validationResult = validate({ criteria: alertParams.criteria } as any);
const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
Expand All @@ -124,7 +125,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
}, [previewResult, showNoDataResults]);

const hasWarningThreshold = useMemo(
() => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')),
() => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')) ?? false,
[alertParams]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
INFRA_ALERT_PREVIEW_PATH,
METRIC_THRESHOLD_ALERT_TYPE_ID,
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
METRIC_ANOMALY_ALERT_TYPE_ID,
AlertPreviewRequestParams,
AlertPreviewSuccessResponsePayload,
} from '../../../../common/alerting/metrics';

export type PreviewableAlertTypes =
| typeof METRIC_THRESHOLD_ALERT_TYPE_ID
| typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID;
| typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID
| typeof METRIC_ANOMALY_ALERT_TYPE_ID;

export async function getAlertPreview({
fetch,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';
import React, { useState, useCallback, useMemo } from 'react';
import {
EuiPopover,
EuiButtonEmpty,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useInfraMLCapabilities } from '../../../containers/ml/infra_ml_capabilities';
import { PrefilledInventoryAlertFlyout } from '../../inventory/components/alert_flyout';
import { PrefilledThresholdAlertFlyout } from '../../metric_threshold/components/alert_flyout';
import { PrefilledAnomalyAlertFlyout } from '../../metric_anomaly/components/alert_flyout';
import { useLinkProps } from '../../../hooks/use_link_props';

type VisibleFlyoutType = 'inventory' | 'threshold' | 'anomaly' | null;

export const MetricsAlertDropdown = () => {
const [popoverOpen, setPopoverOpen] = useState(false);
const [visibleFlyoutType, setVisibleFlyoutType] = useState<VisibleFlyoutType>(null);
const { hasInfraMLCapabilities } = useInfraMLCapabilities();

const closeFlyout = useCallback(() => setVisibleFlyoutType(null), [setVisibleFlyoutType]);

const manageAlertsLinkProps = useLinkProps({
app: 'management',
pathname: '/insightsAndAlerting/triggersActions/alerts',
});

const panels: EuiContextMenuPanelDescriptor[] = useMemo(
() => [
{
id: 0,
title: i18n.translate('xpack.infra.alerting.alertDropdownTitle', {
defaultMessage: 'Alerts',
}),
items: [
{
name: i18n.translate('xpack.infra.alerting.infrastructureDropdownMenu', {
defaultMessage: 'Infrastructure',
}),
panel: 1,
},
{
name: i18n.translate('xpack.infra.alerting.metricsDropdownMenu', {
defaultMessage: 'Metrics',
}),
panel: 2,
},
{
name: i18n.translate('xpack.infra.alerting.manageAlerts', {
defaultMessage: 'Manage alerts',
}),
icon: 'tableOfContents',
onClick: manageAlertsLinkProps.onClick,
},
],
},
{
id: 1,
title: i18n.translate('xpack.infra.alerting.infrastructureDropdownTitle', {
defaultMessage: 'Infrastructure alerts',
}),
items: [
{
name: i18n.translate('xpack.infra.alerting.createInventoryAlertButton', {
defaultMessage: 'Create inventory alert',
}),
onClick: () => setVisibleFlyoutType('inventory'),
},
].concat(
hasInfraMLCapabilities
? {
name: i18n.translate('xpack.infra.alerting.createAnomalyAlertButton', {
defaultMessage: 'Create anomaly alert',
}),
onClick: () => setVisibleFlyoutType('anomaly'),
}
: []
),
},
{
id: 2,
title: i18n.translate('xpack.infra.alerting.metricsDropdownTitle', {
defaultMessage: 'Metrics alerts',
}),
items: [
{
name: i18n.translate('xpack.infra.alerting.createThresholdAlertButton', {
defaultMessage: 'Create threshold alert',
}),
onClick: () => setVisibleFlyoutType('threshold'),
},
],
},
],
[manageAlertsLinkProps, setVisibleFlyoutType, hasInfraMLCapabilities]
);

const closePopover = useCallback(() => {
setPopoverOpen(false);
}, [setPopoverOpen]);

const openPopover = useCallback(() => {
setPopoverOpen(true);
}, [setPopoverOpen]);

return (
<>
<EuiPopover
panelPaddingSize="none"
anchorPosition="downLeft"
button={
<EuiButtonEmpty iconSide={'right'} iconType={'arrowDown'} onClick={openPopover}>
<FormattedMessage id="xpack.infra.alerting.alertsButton" defaultMessage="Alerts" />
</EuiButtonEmpty>
}
isOpen={popoverOpen}
closePopover={closePopover}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
<AlertFlyout visibleFlyoutType={visibleFlyoutType} onClose={closeFlyout} />
</>
);
};

interface AlertFlyoutProps {
visibleFlyoutType: VisibleFlyoutType;
onClose(): void;
}

const AlertFlyout = ({ visibleFlyoutType, onClose }: AlertFlyoutProps) => {
switch (visibleFlyoutType) {
case 'inventory':
return <PrefilledInventoryAlertFlyout onClose={onClose} />;
case 'threshold':
return <PrefilledThresholdAlertFlyout onClose={onClose} />;
case 'anomaly':
return <PrefilledAnomalyAlertFlyout onClose={onClose} />;
default:
return null;
}
};
Loading

0 comments on commit fdd759d

Please sign in to comment.