diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts index 4862b2a7e6a5..5cc976969d79 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/types.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts @@ -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< diff --git a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx index 877d047c941d..02c3ea29c184 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/alert_preview.tsx @@ -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; @@ -45,6 +46,7 @@ export const AlertPreview: React.FC = (props) => { const { alertParams, alertInterval, + alertThrottle, fetch, alertType, validate, @@ -73,16 +75,27 @@ export const AlertPreview: React.FC = (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) => { 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 ( = (props) => { <> - {previewResult.resultTotals.fired}{' '} - {previewResult.resultTotals.fired === 1 - ? firedTimeLabel - : firedTimesLabel} + ), }} @@ -173,7 +196,7 @@ export const AlertPreview: React.FC = (props) => { ) : null} e.value === previewResult.previewLookbackInterval @@ -211,6 +234,32 @@ export const AlertPreview: React.FC = (props) => { defaultMessage="An error occurred when trying to evaluate some of the data." /> ) : null} + {showNumberOfNotifications ? ( + <> + + + {i18n.translate( + 'xpack.infra.metrics.alertFlyout.alertPreviewTotalNotificationsNumber', + { + defaultMessage: + '{notifs, plural, one {# notification} other {# notifications}}', + values: { + notifs: previewResult.resultTotals.notifications, + }, + } + )} + + ), + }} + /> + + ) : null}{' '} )} @@ -218,6 +267,7 @@ export const AlertPreview: React.FC = (props) => { <> = (props) => { {previewError.body?.statusCode === 508 ? ( = (props) => { ) : ( = 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', -}); diff --git a/x-pack/plugins/infra/public/alerting/common/index.ts b/x-pack/plugins/infra/public/alerting/common/index.ts index e1b4a70cfb1f..384391578f0c 100644 --- a/x-pack/plugins/infra/public/alerting/common/index.ts +++ b/x-pack/plugins/infra/public/alerting/common/index.ts @@ -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', -}); diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx index ada7a30a859e..60a00371e5ad 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.test.tsx @@ -69,6 +69,7 @@ describe('Expression', () => { Reflect.set(alertParams, key, value)} diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index 5ac2f407839e..f47f30c280b2 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -89,6 +89,7 @@ interface Props { alertOnNoData?: boolean; }; alertInterval: string; + alertThrottle: string; alertsContext: AlertsContextValue; 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) => { - 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) => { { Reflect.set(alertParams, key, value)} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 6b102045fa51..c71a3b6b1333 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -51,6 +51,7 @@ interface Props { alertParams: AlertParams; alertsContext: AlertsContextValue; 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) => { - 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) => { { 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; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts index c26b44dfe8ff..73e17537476c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.test.ts @@ -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); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts index 0f2afda663da..e1615625d605 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts @@ -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; diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 40d09dadfe05..1233e9d2d135 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -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) => { 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, } ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 868fa8a7e617..bd4b81608acd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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時間", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8bd3fcb7c3a3..fbbd8ae4053b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "上一小时", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 6177262557e0..c69c33c0fe22 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -248,6 +248,7 @@ export const AlertForm = ({ { alertParams: AlertParamsType; alertInterval: string; + alertThrottle: string; setAlertParams: (property: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; errors: IErrorObject;