Skip to content

Commit

Permalink
[Metrics UI] Add inventory alert preview (#68909) (#69766)
Browse files Browse the repository at this point in the history
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
Zacqary and elasticmachine committed Jun 24, 2020
1 parent 89ba7ff commit 30b997f
Show file tree
Hide file tree
Showing 20 changed files with 563 additions and 208 deletions.
20 changes: 18 additions & 2 deletions x-pack/plugins/infra/common/alerting/metrics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

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

// TODO: Have threshold and inventory alerts import these types from this file instead of from their
// local directories
Expand Down Expand Up @@ -39,7 +40,16 @@ const baseAlertRequestParamsRT = rt.intersection([
sourceId: rt.string,
}),
rt.type({
lookback: rt.union([rt.literal('h'), rt.literal('d'), rt.literal('w'), rt.literal('M')]),
lookback: rt.union([
rt.literal('ms'),
rt.literal('s'),
rt.literal('m'),
rt.literal('h'),
rt.literal('d'),
rt.literal('w'),
rt.literal('M'),
rt.literal('y'),
]),
criteria: rt.array(rt.any),
alertInterval: rt.string,
}),
Expand All @@ -61,10 +71,13 @@ export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf<
const inventoryAlertPreviewRequestParamsRT = rt.intersection([
baseAlertRequestParamsRT,
rt.type({
nodeType: rt.string,
nodeType: ItemTypeRT,
alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID),
}),
]);
export type InventoryAlertPreviewRequestParams = rt.TypeOf<
typeof inventoryAlertPreviewRequestParamsRT
>;

export const alertPreviewRequestParamsRT = rt.union([
metricThresholdAlertPreviewRequestParamsRT,
Expand All @@ -80,3 +93,6 @@ export const alertPreviewSuccessResponsePayloadRT = rt.type({
tooManyBuckets: rt.number,
}),
});
export type AlertPreviewSuccessResponsePayload = rt.TypeOf<
typeof alertPreviewSuccessResponsePayloadRT
>;
51 changes: 51 additions & 0 deletions x-pack/plugins/infra/public/alerting/common/get_alert_preview.ts
Original file line number Diff line number Diff line change
@@ -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 * as rt from 'io-ts';
import { HttpSetup } from 'src/core/public';
import {
INFRA_ALERT_PREVIEW_PATH,
METRIC_THRESHOLD_ALERT_TYPE_ID,
METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
alertPreviewRequestParamsRT,
alertPreviewSuccessResponsePayloadRT,
} from '../../../common/alerting/metrics';

async function getAlertPreview({
fetch,
params,
alertType,
}: {
fetch: HttpSetup['fetch'];
params: rt.TypeOf<typeof alertPreviewRequestParamsRT>;
alertType:
| typeof METRIC_THRESHOLD_ALERT_TYPE_ID
| typeof METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID;
}): Promise<rt.TypeOf<typeof alertPreviewSuccessResponsePayloadRT>> {
return await fetch(`${INFRA_ALERT_PREVIEW_PATH}`, {
method: 'POST',
body: JSON.stringify({
...params,
alertType,
}),
});
}

export const getMetricThresholdAlertPreview = ({
fetch,
params,
}: {
fetch: HttpSetup['fetch'];
params: rt.TypeOf<typeof alertPreviewRequestParamsRT>;
}) => getAlertPreview({ fetch, params, alertType: METRIC_THRESHOLD_ALERT_TYPE_ID });

export const getInventoryAlertPreview = ({
fetch,
params,
}: {
fetch: HttpSetup['fetch'];
params: rt.TypeOf<typeof alertPreviewRequestParamsRT>;
}) => getAlertPreview({ fetch, params, alertType: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID });
55 changes: 55 additions & 0 deletions x-pack/plugins/infra/public/alerting/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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';

export * from './get_alert_preview';

export const previewOptions = [
{
value: 'h',
text: i18n.translate('xpack.infra.metrics.alertFlyout.lastHourLabel', {
defaultMessage: 'Last hour',
}),
shortText: i18n.translate('xpack.infra.metrics.alertFlyout.hourLabel', {
defaultMessage: 'hour',
}),
},
{
value: 'd',
text: i18n.translate('xpack.infra.metrics.alertFlyout.lastDayLabel', {
defaultMessage: 'Last day',
}),
shortText: i18n.translate('xpack.infra.metrics.alertFlyout.dayLabel', {
defaultMessage: 'day',
}),
},
{
value: 'w',
text: i18n.translate('xpack.infra.metrics.alertFlyout.lastWeekLabel', {
defaultMessage: 'Last week',
}),
shortText: i18n.translate('xpack.infra.metrics.alertFlyout.weekLabel', {
defaultMessage: 'week',
}),
},
{
value: 'M',
text: i18n.translate('xpack.infra.metrics.alertFlyout.lastMonthLabel', {
defaultMessage: 'Last month',
}),
shortText: i18n.translate('xpack.infra.metrics.alertFlyout.monthLabel', {
defaultMessage: 'month',
}),
},
];

export const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
defaultMessage: 'time',
});
export const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
defaultMessage: 'times',
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { debounce } from 'lodash';
import { debounce, pick } from 'lodash';
import { Unit } from '@elastic/datemath';
import React, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react';
import {
EuiFlexGroup,
Expand All @@ -15,9 +16,20 @@ import {
EuiFormRow,
EuiButtonEmpty,
EuiFieldSearch,
EuiSelect,
EuiButton,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
previewOptions,
firedTimeLabel,
firedTimesLabel,
getInventoryAlertPreview as getAlertPreview,
} from '../../../alerting/common';
import { AlertPreviewSuccessResponsePayload } from '../../../../common/alerting/metrics/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { getIntervalInSeconds } from '../../../../server/utils/get_interval_in_seconds';
import {
Comparator,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
Expand Down Expand Up @@ -52,6 +64,8 @@ import { NodeTypeExpression } from './node_type';
import { InfraWaffleMapOptions } from '../../../lib/lib';
import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';

import { validateMetricThreshold } from './validation';

const FILTER_TYPING_DEBOUNCE_MS = 500;

interface AlertContextMeta {
Expand All @@ -65,18 +79,16 @@ interface Props {
alertParams: {
criteria: InventoryMetricConditions[];
nodeType: InventoryItemType;
groupBy?: string;
filterQuery?: string;
filterQueryText?: string;
sourceId?: string;
};
alertInterval: string;
alertsContext: AlertsContextValue<AlertContextMeta>;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
}

type TimeUnit = 's' | 'm' | 'h' | 'd';

const defaultExpression = {
metric: 'cpu' as SnapshotMetricType,
comparator: Comparator.GT,
Expand All @@ -86,15 +98,40 @@ const defaultExpression = {
} as InventoryMetricConditions;

export const Expressions: React.FC<Props> = (props) => {
const { setAlertParams, alertParams, errors, alertsContext } = props;
const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
fetch: alertsContext.http.fetch,
toastWarning: alertsContext.toastNotifications.addWarning,
});
const [timeSize, setTimeSize] = useState<number | undefined>(1);
const [timeUnit, setTimeUnit] = useState<TimeUnit>('m');
const [timeUnit, setTimeUnit] = useState<Unit>('m');

const [previewLookbackInterval, setPreviewLookbackInterval] = useState<string>('h');
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(false);
const [previewError, setPreviewError] = useState<boolean>(false);
const [previewResult, setPreviewResult] = useState<AlertPreviewSuccessResponsePayload | null>(
null
);

const previewIntervalError = useMemo(() => {
const intervalInSeconds = getIntervalInSeconds(alertInterval);
const lookbackInSeconds = getIntervalInSeconds(`1${previewLookbackInterval}`);
if (intervalInSeconds >= lookbackInSeconds) {
return true;
}
return false;
}, [previewLookbackInterval, alertInterval]);

const isPreviewDisabled = useMemo(() => {
if (previewIntervalError) return true;
const validationResult = validateMetricThreshold({ criteria: alertParams.criteria } as any);
const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
);
return hasValidationErrors;
}, [alertParams.criteria, previewIntervalError]);

const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
createDerivedIndexPattern,
Expand Down Expand Up @@ -173,7 +210,7 @@ export const Expressions: React.FC<Props> = (props) => {
...c,
timeUnit: tu,
}));
setTimeUnit(tu as TimeUnit);
setTimeUnit(tu as Unit);
setAlertParams('criteria', criteria);
},
[alertParams.criteria, setAlertParams]
Expand Down Expand Up @@ -216,6 +253,33 @@ export const Expressions: React.FC<Props> = (props) => {
}
}, [alertsContext.metadata, derivedIndexPattern, setAlertParams]);

const onSelectPreviewLookbackInterval = useCallback((e) => {
setPreviewLookbackInterval(e.target.value);
setPreviewResult(null);
}, []);

const onClickPreview = useCallback(async () => {
setIsPreviewLoading(true);
setPreviewResult(null);
setPreviewError(false);
try {
const result = await getAlertPreview({
fetch: alertsContext.http.fetch,
params: {
...pick(alertParams, 'criteria', 'nodeType'),
sourceId: alertParams.sourceId,
lookback: previewLookbackInterval as Unit,
alertInterval,
},
});
setPreviewResult(result);
} catch (e) {
setPreviewError(true);
} finally {
setIsPreviewLoading(false);
}
}, [alertParams, alertInterval, alertsContext, previewLookbackInterval]);

useEffect(() => {
const md = alertsContext.metadata;
if (!alertParams.nodeType) {
Expand Down Expand Up @@ -332,6 +396,91 @@ export const Expressions: React.FC<Props> = (props) => {
</EuiFormRow>

<EuiSpacer size={'m'} />
<EuiFormRow
label={i18n.translate('xpack.infra.metrics.alertFlyout.previewLabel', {
defaultMessage: 'Preview',
})}
fullWidth
compressed
>
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiSelect
id="selectPreviewLookbackInterval"
value={previewLookbackInterval}
onChange={onSelectPreviewLookbackInterval}
options={previewOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isLoading={isPreviewLoading}
isDisabled={isPreviewDisabled}
onClick={onClickPreview}
>
{i18n.translate('xpack.infra.metrics.alertFlyout.testAlertTrigger', {
defaultMessage: 'Test alert trigger',
})}
</EuiButton>
</EuiFlexItem>
<EuiSpacer size={'s'} />
</EuiFlexGroup>
{previewResult && (
<>
<EuiSpacer size={'s'} />
<EuiText>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewResult"
defaultMessage="This alert would have fired {fired} {timeOrTimes} in the past {lookback}"
values={{
timeOrTimes:
previewResult.resultTotals.fired === 1 ? firedTimeLabel : firedTimesLabel,
fired: <strong>{previewResult.resultTotals.fired}</strong>,
lookback: previewOptions.find((e) => e.value === previewLookbackInterval)
?.shortText,
}}
/>{' '}
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewGroups"
defaultMessage="across {numberOfGroups} {groupName}{plural}."
values={{
numberOfGroups: <strong>{previewResult.numberOfGroups}</strong>,
groupName: alertParams.nodeType,
plural: previewResult.numberOfGroups !== 1 ? 's' : '',
}}
/>
</EuiText>
</>
)}
{previewIntervalError && (
<>
<EuiSpacer size={'s'} />
<EuiText>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.previewIntervalTooShort"
defaultMessage="Not enough data to preview. Please select a longer preview length, or increase the amount of time in the {checkEvery} field."
values={{
checkEvery: <strong>check every</strong>,
}}
/>
</EuiText>
</>
)}
{previewError && (
<>
<EuiSpacer size={'s'} />
<EuiText>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewError"
defaultMessage="An error occurred when trying to preview this alert trigger."
/>
</EuiText>
</>
)}
</>
</EuiFormRow>
<EuiSpacer size={'m'} />
</>
);
};
Expand Down
Loading

0 comments on commit 30b997f

Please sign in to comment.