[Metrics UI] Use Notify Every in Alert Preview (#74401)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Zacqary Adam Xeper 2020-09-18 14:42:23 -05:00 committed by GitHub
parent 060f0895cc
commit 9cf546f3d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 225 additions and 48 deletions

View file

@ -52,6 +52,8 @@ const baseAlertRequestParamsRT = rt.intersection([
]),
criteria: rt.array(rt.any),
alertInterval: rt.string,
alertThrottle: rt.string,
alertOnNoData: rt.boolean,
}),
]);
@ -91,6 +93,7 @@ export const alertPreviewSuccessResponsePayloadRT = rt.type({
fired: rt.number,
noData: rt.number,
error: rt.number,
notifications: rt.number,
}),
});
export type AlertPreviewSuccessResponsePayload = rt.TypeOf<

View file

@ -33,6 +33,7 @@ import { getAlertPreview, PreviewableAlertTypes } from './get_alert_preview';
interface Props {
alertInterval: string;
alertThrottle: string;
alertType: PreviewableAlertTypes;
fetch: HttpSetup['fetch'];
alertParams: { criteria: any[]; sourceId: string } & Record<string, any>;
@ -45,6 +46,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
const {
alertParams,
alertInterval,
alertThrottle,
fetch,
alertType,
validate,
@ -73,16 +75,27 @@ export const AlertPreview: React.FC<Props> = (props) => {
...alertParams,
lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
alertInterval,
alertThrottle,
alertOnNoData: showNoDataResults ?? false,
} as AlertPreviewRequestParams,
alertType,
});
setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval });
setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval, alertThrottle });
} catch (e) {
setPreviewError(e);
} finally {
setIsPreviewLoading(false);
}
}, [alertParams, alertInterval, fetch, alertType, groupByDisplayName, previewLookbackInterval]);
}, [
alertParams,
alertInterval,
fetch,
alertType,
groupByDisplayName,
previewLookbackInterval,
alertThrottle,
showNoDataResults,
]);
const previewIntervalError = useMemo(() => {
const intervalInSeconds = getIntervalInSeconds(alertInterval);
@ -101,6 +114,13 @@ export const AlertPreview: React.FC<Props> = (props) => {
return hasValidationErrors || previewIntervalError;
}, [alertParams.criteria, previewIntervalError, validate]);
const showNumberOfNotifications = useMemo(() => {
if (!previewResult) return false;
const { notifications, fired, noData, error } = previewResult.resultTotals;
const unthrottledNotifications = fired + (showNoDataResults ? noData + error : 0);
return unthrottledNotifications > notifications;
}, [previewResult, showNoDataResults]);
return (
<EuiFormRow
label={i18n.translate('xpack.infra.metrics.alertFlyout.previewLabel', {
@ -136,19 +156,22 @@ export const AlertPreview: React.FC<Props> = (props) => {
<>
<EuiSpacer size={'s'} />
<EuiCallOut
iconType="iInCircle"
size="s"
title={
<>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewResult"
defaultMessage="This alert would have occurred {firedTimes}"
defaultMessage="There were {firedTimes}"
values={{
firedTimes: (
<strong>
{previewResult.resultTotals.fired}{' '}
{previewResult.resultTotals.fired === 1
? firedTimeLabel
: firedTimesLabel}
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.firedTimes"
defaultMessage="{fired, plural, one {# instance} other {# instances}}"
values={{
fired: previewResult.resultTotals.fired,
}}
/>
</strong>
),
}}
@ -173,7 +196,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
) : null}
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewResultLookback"
defaultMessage="in the last {lookback}."
defaultMessage="that satisfied the conditions of this alert in the last {lookback}."
values={{
lookback: previewOptions.find(
(e) => e.value === previewResult.previewLookbackInterval
@ -211,6 +234,32 @@ export const AlertPreview: React.FC<Props> = (props) => {
defaultMessage="An error occurred when trying to evaluate some of the data."
/>
) : null}
{showNumberOfNotifications ? (
<>
<EuiSpacer size={'s'} />
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewTotalNotifications"
defaultMessage='As a result, this alert would have sent {notifications} based on the selected "notify every" setting of "{alertThrottle}."'
values={{
alertThrottle: previewResult.alertThrottle,
notifications: (
<strong>
{i18n.translate(
'xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber',
{
defaultMessage:
'{notifs, plural, one {# notification} other {# notifications}}',
values: {
notifs: previewResult.resultTotals.notifications,
},
}
)}
</strong>
),
}}
/>
</>
) : null}{' '}
</EuiCallOut>
</>
)}
@ -218,6 +267,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
<>
<EuiSpacer size={'s'} />
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.previewIntervalTooShortTitle"
@ -242,6 +292,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
<EuiSpacer size={'s'} />
{previewError.body?.statusCode === 508 ? (
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.tooManyBucketsErrorTitle"
@ -264,6 +315,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
</EuiCallOut>
) : (
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewError"
@ -349,10 +401,3 @@ const previewOptions = [
const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) =>
omit(o, 'shortText')
);
const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
defaultMessage: 'time',
});
const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
defaultMessage: 'times',
});

View file

@ -45,10 +45,3 @@ export const previewOptions = [
}),
},
];
export const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
defaultMessage: 'time',
});
export const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
defaultMessage: 'times',
});

View file

@ -69,6 +69,7 @@ describe('Expression', () => {
<Expressions
alertsContext={context}
alertInterval="1m"
alertThrottle="1m"
alertParams={alertParams as any}
errors={[]}
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}

View file

@ -89,6 +89,7 @@ interface Props {
alertOnNoData?: boolean;
};
alertInterval: string;
alertThrottle: string;
alertsContext: AlertsContextValue<AlertContextMeta>;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
@ -104,7 +105,14 @@ const defaultExpression = {
} as InventoryMetricConditions;
export const Expressions: React.FC<Props> = (props) => {
const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
const {
setAlertParams,
alertParams,
errors,
alertsContext,
alertInterval,
alertThrottle,
} = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
@ -378,6 +386,7 @@ export const Expressions: React.FC<Props> = (props) => {
<EuiSpacer size={'m'} />
<AlertPreview
alertInterval={alertInterval}
alertThrottle={alertThrottle}
alertType={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
alertParams={pick(alertParams, 'criteria', 'nodeType', 'sourceId', 'filterQuery')}
validate={validateMetricThreshold}

View file

@ -68,6 +68,7 @@ describe('Expression', () => {
<Expressions
alertsContext={context}
alertInterval="1m"
alertThrottle="1m"
alertParams={alertParams}
errors={[]}
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}

View file

@ -51,6 +51,7 @@ interface Props {
alertParams: AlertParams;
alertsContext: AlertsContextValue<AlertContextMeta>;
alertInterval: string;
alertThrottle: string;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
}
@ -65,7 +66,14 @@ const defaultExpression = {
export { defaultExpression };
export const Expressions: React.FC<Props> = (props) => {
const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
const {
setAlertParams,
alertParams,
errors,
alertsContext,
alertInterval,
alertThrottle,
} = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
@ -399,6 +407,7 @@ export const Expressions: React.FC<Props> = (props) => {
<EuiSpacer size={'m'} />
<AlertPreview
alertInterval={alertInterval}
alertThrottle={alertThrottle}
alertType={METRIC_THRESHOLD_ALERT_TYPE_ID}
alertParams={pick(alertParams, 'criteria', 'groupBy', 'filterQuery', 'sourceId')}
showNoDataResults={alertParams.alertOnNoData}

View file

@ -29,6 +29,8 @@ interface PreviewInventoryMetricThresholdAlertParams {
source: InfraSource;
lookback: Unit;
alertInterval: string;
alertThrottle: string;
alertOnNoData: boolean;
}
export const previewInventoryMetricThresholdAlert = async ({
@ -37,6 +39,8 @@ export const previewInventoryMetricThresholdAlert = async ({
source,
lookback,
alertInterval,
alertThrottle,
alertOnNoData,
}: PreviewInventoryMetricThresholdAlertParams) => {
const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams;
@ -52,6 +56,10 @@ export const previewInventoryMetricThresholdAlert = async ({
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle);
const executionsPerThrottle = Math.floor(
(throttleIntervalInSeconds / alertIntervalInSeconds) * alertResultsPerExecution
);
try {
const results = await Promise.all(
criteria.map((c) =>
@ -66,6 +74,12 @@ export const previewInventoryMetricThresholdAlert = async ({
let numberOfTimesFired = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
let numberOfNotifications = 0;
let throttleTracker = 0;
const notifyWithThrottle = () => {
if (throttleTracker === 0) numberOfNotifications++;
throttleTracker++;
};
for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
const allConditionsFiredInMappedBucket = results.every((result) => {
@ -79,11 +93,27 @@ export const previewInventoryMetricThresholdAlert = async ({
const someConditionsErrorInMappedBucket = results.some((result) => {
return result[item].isError;
});
if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
if (someConditionsErrorInMappedBucket) numberOfErrors++;
if (someConditionsErrorInMappedBucket) {
numberOfErrors++;
if (alertOnNoData) {
notifyWithThrottle();
}
} else if (someConditionsNoDataInMappedBucket) {
numberOfNoDataResults++;
if (alertOnNoData) {
notifyWithThrottle();
}
} else if (allConditionsFiredInMappedBucket) {
numberOfTimesFired++;
notifyWithThrottle();
} else if (throttleTracker > 0) {
throttleTracker++;
}
if (throttleTracker === executionsPerThrottle) {
throttleTracker = 0;
}
}
return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors];
return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications];
});
return previewResults;

View file

@ -16,11 +16,14 @@ describe('Previewing the metric threshold alert type', () => {
...baseParams,
lookback: 'h',
alertInterval: '1m',
alertThrottle: '1m',
alertOnNoData: true,
});
const [firedResults, noDataResults, errorResults] = ungroupedResult;
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(30);
expect(noDataResults).toBe(0);
expect(errorResults).toBe(0);
expect(notifications).toBe(30);
});
test('returns the expected results using a bucket interval shorter than the alert interval', async () => {
@ -28,22 +31,42 @@ describe('Previewing the metric threshold alert type', () => {
...baseParams,
lookback: 'h',
alertInterval: '3m',
alertThrottle: '3m',
alertOnNoData: true,
});
const [firedResults, noDataResults, errorResults] = ungroupedResult;
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(10);
expect(noDataResults).toBe(0);
expect(errorResults).toBe(0);
expect(notifications).toBe(10);
});
test('returns the expected results using a bucket interval longer than the alert interval', async () => {
const [ungroupedResult] = await previewMetricThresholdAlert({
...baseParams,
lookback: 'h',
alertInterval: '30s',
alertThrottle: '30s',
alertOnNoData: true,
});
const [firedResults, noDataResults, errorResults] = ungroupedResult;
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(60);
expect(noDataResults).toBe(0);
expect(errorResults).toBe(0);
expect(notifications).toBe(60);
});
test('returns the expected results using a throttle interval longer than the alert interval', async () => {
const [ungroupedResult] = await previewMetricThresholdAlert({
...baseParams,
lookback: 'h',
alertInterval: '1m',
alertThrottle: '3m',
alertOnNoData: true,
});
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(30);
expect(noDataResults).toBe(0);
expect(errorResults).toBe(0);
expect(notifications).toBe(15);
});
});
describe('querying with a groupBy parameter', () => {
@ -56,15 +79,19 @@ describe('Previewing the metric threshold alert type', () => {
},
lookback: 'h',
alertInterval: '1m',
alertThrottle: '1m',
alertOnNoData: true,
});
const [firedResultsA, noDataResultsA, errorResultsA] = resultA;
const [firedResultsA, noDataResultsA, errorResultsA, notificationsA] = resultA;
expect(firedResultsA).toBe(30);
expect(noDataResultsA).toBe(0);
expect(errorResultsA).toBe(0);
const [firedResultsB, noDataResultsB, errorResultsB] = resultB;
expect(notificationsA).toBe(30);
const [firedResultsB, noDataResultsB, errorResultsB, notificationsB] = resultB;
expect(firedResultsB).toBe(60);
expect(noDataResultsB).toBe(0);
expect(errorResultsB).toBe(0);
expect(notificationsB).toBe(60);
});
});
describe('querying a data set with a period of No Data', () => {
@ -82,11 +109,14 @@ describe('Previewing the metric threshold alert type', () => {
},
lookback: 'h',
alertInterval: '1m',
alertThrottle: '1m',
alertOnNoData: true,
});
const [firedResults, noDataResults, errorResults] = ungroupedResult;
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
expect(firedResults).toBe(25);
expect(noDataResults).toBe(10);
expect(errorResults).toBe(0);
expect(notifications).toBe(35);
});
});
});

View file

@ -28,6 +28,8 @@ interface PreviewMetricThresholdAlertParams {
config: InfraSource['configuration'];
lookback: Unit;
alertInterval: string;
alertThrottle: string;
alertOnNoData: boolean;
end?: number;
overrideLookbackIntervalInSeconds?: number;
}
@ -43,6 +45,8 @@ export const previewMetricThresholdAlert: (
config,
lookback,
alertInterval,
alertThrottle,
alertOnNoData,
end = Date.now(),
overrideLookbackIntervalInSeconds,
},
@ -77,6 +81,11 @@ export const previewMetricThresholdAlert: (
// Now determine how to interpolate this histogram based on the alert interval
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
const throttleIntervalInSeconds = Math.max(
getIntervalInSeconds(alertThrottle),
alertIntervalInSeconds
);
const previewResults = await Promise.all(
groups.map(async (group) => {
// Interpolate the buckets returned by evaluateAlert and return a count of how many of these
@ -90,6 +99,12 @@ export const previewMetricThresholdAlert: (
let numberOfTimesFired = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
let numberOfNotifications = 0;
let throttleTracker = 0;
const notifyWithThrottle = () => {
if (throttleTracker === 0) numberOfNotifications++;
throttleTracker += alertIntervalInSeconds;
};
for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
const allConditionsFiredInMappedBucket = alertResults.every(
@ -102,11 +117,27 @@ export const previewMetricThresholdAlert: (
const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => {
return alertResult[group].isError;
});
if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
if (someConditionsErrorInMappedBucket) numberOfErrors++;
if (someConditionsErrorInMappedBucket) {
numberOfErrors++;
if (alertOnNoData) {
notifyWithThrottle();
}
} else if (someConditionsNoDataInMappedBucket) {
numberOfNoDataResults++;
if (alertOnNoData) {
notifyWithThrottle();
}
} else if (allConditionsFiredInMappedBucket) {
numberOfTimesFired++;
notifyWithThrottle();
} else if (throttleTracker > 0) {
throttleTracker += alertIntervalInSeconds;
}
if (throttleTracker >= throttleIntervalInSeconds) {
throttleTracker = 0;
}
}
return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors];
return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors, numberOfNotifications];
})
);
return previewResults;
@ -114,7 +145,15 @@ export const previewMetricThresholdAlert: (
if (isTooManyBucketsPreviewException(e)) {
// If there's too much data on the first request, recursively slice the lookback interval
// until all the data can be retrieved
const basePreviewParams = { callCluster, params, config, lookback, alertInterval };
const basePreviewParams = {
callCluster,
params,
config,
lookback,
alertInterval,
alertThrottle,
alertOnNoData,
};
const { maxBuckets } = e;
// If this is still the first iteration, try to get the number of groups in order to
// calculate max buckets. If this fails, just estimate based on 1 group
@ -159,7 +198,7 @@ export const previewMetricThresholdAlert: (
.reduce((a, b) => {
if (!a) return b;
if (!b) return a;
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
return [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]];
})
);
return zippedResult;

View file

@ -30,7 +30,16 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
},
},
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
const { criteria, filterQuery, lookback, sourceId, alertType, alertInterval } = request.body;
const {
criteria,
filterQuery,
lookback,
sourceId,
alertType,
alertInterval,
alertThrottle,
alertOnNoData,
} = request.body;
const callCluster = (endpoint: string, opts: Record<string, any>) => {
return callWithRequest(requestContext, endpoint, opts);
@ -51,22 +60,26 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
lookback,
config: source.configuration,
alertInterval,
alertThrottle,
alertOnNoData,
});
const numberOfGroups = previewResult.length;
const resultTotals = previewResult.reduce(
(totals, [firedResult, noDataResult, errorResult]) => {
(totals, [firedResult, noDataResult, errorResult, notifications]) => {
return {
...totals,
fired: totals.fired + firedResult,
noData: totals.noData + noDataResult,
error: totals.error + errorResult,
notifications: totals.notifications + notifications,
};
},
{
fired: 0,
noData: 0,
error: 0,
notifications: 0,
}
);
return response.ok({
@ -84,22 +97,26 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
lookback,
source,
alertInterval,
alertThrottle,
alertOnNoData,
});
const numberOfGroups = previewResult.length;
const resultTotals = previewResult.reduce(
(totals, [firedResult, noDataResult, errorResult]) => {
(totals, [firedResult, noDataResult, errorResult, notifications]) => {
return {
...totals,
fired: totals.fired + firedResult,
noData: totals.noData + noDataResult,
error: totals.error + errorResult,
notifications: totals.notifications + notifications,
};
},
{
fired: 0,
noData: 0,
error: 0,
notifications: 0,
}
);

View file

@ -8677,8 +8677,7 @@
"xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "タイミング",
"xpack.infra.metrics.alertFlyout.filterHelpText": "KQL式を使用して、アラートトリガーの範囲を制限します。",
"xpack.infra.metrics.alertFlyout.filterLabel": "フィルター(任意)",
"xpack.infra.metrics.alertFlyout.firedTime": "時間",
"xpack.infra.metrics.alertFlyout.firedTimes": "回数",
"xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 時間} other {# 回数}}",
"xpack.infra.metrics.alertFlyout.hourLabel": "時間",
"xpack.infra.metrics.alertFlyout.lastDayLabel": "昨日",
"xpack.infra.metrics.alertFlyout.lastHourLabel": "過去1時間",

View file

@ -8683,8 +8683,7 @@
"xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "当",
"xpack.infra.metrics.alertFlyout.filterHelpText": "使用 KQL 表达式限制告警触发器的范围。",
"xpack.infra.metrics.alertFlyout.filterLabel": "筛选(可选)",
"xpack.infra.metrics.alertFlyout.firedTime": "次",
"xpack.infra.metrics.alertFlyout.firedTimes": "次",
"xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 次} other {# 次}}",
"xpack.infra.metrics.alertFlyout.hourLabel": "小时",
"xpack.infra.metrics.alertFlyout.lastDayLabel": "昨天",
"xpack.infra.metrics.alertFlyout.lastHourLabel": "上一小时",

View file

@ -248,6 +248,7 @@ export const AlertForm = ({
<AlertParamsExpressionComponent
alertParams={alert.params}
alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`}
alertThrottle={`${alertThrottle ?? 1}${alertThrottleUnit}`}
errors={errors}
setAlertParams={setAlertParams}
setAlertProperty={setAlertProperty}

View file

@ -134,6 +134,7 @@ export interface AlertTypeParamsExpressionProps<
> {
alertParams: AlertParamsType;
alertInterval: string;
alertThrottle: string;
setAlertParams: (property: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
errors: IErrorObject;