[Metrics UI] Use Notify Every in Alert Preview (#74401)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
060f0895cc
commit
9cf546f3d0
|
@ -52,6 +52,8 @@ const baseAlertRequestParamsRT = rt.intersection([
|
||||||
]),
|
]),
|
||||||
criteria: rt.array(rt.any),
|
criteria: rt.array(rt.any),
|
||||||
alertInterval: rt.string,
|
alertInterval: rt.string,
|
||||||
|
alertThrottle: rt.string,
|
||||||
|
alertOnNoData: rt.boolean,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -91,6 +93,7 @@ export const alertPreviewSuccessResponsePayloadRT = rt.type({
|
||||||
fired: rt.number,
|
fired: rt.number,
|
||||||
noData: rt.number,
|
noData: rt.number,
|
||||||
error: rt.number,
|
error: rt.number,
|
||||||
|
notifications: rt.number,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
export type AlertPreviewSuccessResponsePayload = rt.TypeOf<
|
export type AlertPreviewSuccessResponsePayload = rt.TypeOf<
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { getAlertPreview, PreviewableAlertTypes } from './get_alert_preview';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
alertInterval: string;
|
alertInterval: string;
|
||||||
|
alertThrottle: string;
|
||||||
alertType: PreviewableAlertTypes;
|
alertType: PreviewableAlertTypes;
|
||||||
fetch: HttpSetup['fetch'];
|
fetch: HttpSetup['fetch'];
|
||||||
alertParams: { criteria: any[]; sourceId: string } & Record<string, any>;
|
alertParams: { criteria: any[]; sourceId: string } & Record<string, any>;
|
||||||
|
@ -45,6 +46,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
alertParams,
|
alertParams,
|
||||||
alertInterval,
|
alertInterval,
|
||||||
|
alertThrottle,
|
||||||
fetch,
|
fetch,
|
||||||
alertType,
|
alertType,
|
||||||
validate,
|
validate,
|
||||||
|
@ -73,16 +75,27 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
...alertParams,
|
...alertParams,
|
||||||
lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
|
lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
|
||||||
alertInterval,
|
alertInterval,
|
||||||
|
alertThrottle,
|
||||||
|
alertOnNoData: showNoDataResults ?? false,
|
||||||
} as AlertPreviewRequestParams,
|
} as AlertPreviewRequestParams,
|
||||||
alertType,
|
alertType,
|
||||||
});
|
});
|
||||||
setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval });
|
setPreviewResult({ ...result, groupByDisplayName, previewLookbackInterval, alertThrottle });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setPreviewError(e);
|
setPreviewError(e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsPreviewLoading(false);
|
setIsPreviewLoading(false);
|
||||||
}
|
}
|
||||||
}, [alertParams, alertInterval, fetch, alertType, groupByDisplayName, previewLookbackInterval]);
|
}, [
|
||||||
|
alertParams,
|
||||||
|
alertInterval,
|
||||||
|
fetch,
|
||||||
|
alertType,
|
||||||
|
groupByDisplayName,
|
||||||
|
previewLookbackInterval,
|
||||||
|
alertThrottle,
|
||||||
|
showNoDataResults,
|
||||||
|
]);
|
||||||
|
|
||||||
const previewIntervalError = useMemo(() => {
|
const previewIntervalError = useMemo(() => {
|
||||||
const intervalInSeconds = getIntervalInSeconds(alertInterval);
|
const intervalInSeconds = getIntervalInSeconds(alertInterval);
|
||||||
|
@ -101,6 +114,13 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
return hasValidationErrors || previewIntervalError;
|
return hasValidationErrors || previewIntervalError;
|
||||||
}, [alertParams.criteria, previewIntervalError, validate]);
|
}, [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 (
|
return (
|
||||||
<EuiFormRow
|
<EuiFormRow
|
||||||
label={i18n.translate('xpack.infra.metrics.alertFlyout.previewLabel', {
|
label={i18n.translate('xpack.infra.metrics.alertFlyout.previewLabel', {
|
||||||
|
@ -136,19 +156,22 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
<>
|
<>
|
||||||
<EuiSpacer size={'s'} />
|
<EuiSpacer size={'s'} />
|
||||||
<EuiCallOut
|
<EuiCallOut
|
||||||
iconType="iInCircle"
|
size="s"
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.infra.metrics.alertFlyout.alertPreviewResult"
|
id="xpack.infra.metrics.alertFlyout.alertPreviewResult"
|
||||||
defaultMessage="This alert would have occurred {firedTimes}"
|
defaultMessage="There were {firedTimes}"
|
||||||
values={{
|
values={{
|
||||||
firedTimes: (
|
firedTimes: (
|
||||||
<strong>
|
<strong>
|
||||||
{previewResult.resultTotals.fired}{' '}
|
<FormattedMessage
|
||||||
{previewResult.resultTotals.fired === 1
|
id="xpack.infra.metrics.alertFlyout.firedTimes"
|
||||||
? firedTimeLabel
|
defaultMessage="{fired, plural, one {# instance} other {# instances}}"
|
||||||
: firedTimesLabel}
|
values={{
|
||||||
|
fired: previewResult.resultTotals.fired,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</strong>
|
</strong>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
@ -173,7 +196,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
) : null}
|
) : null}
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.infra.metrics.alertFlyout.alertPreviewResultLookback"
|
id="xpack.infra.metrics.alertFlyout.alertPreviewResultLookback"
|
||||||
defaultMessage="in the last {lookback}."
|
defaultMessage="that satisfied the conditions of this alert in the last {lookback}."
|
||||||
values={{
|
values={{
|
||||||
lookback: previewOptions.find(
|
lookback: previewOptions.find(
|
||||||
(e) => e.value === previewResult.previewLookbackInterval
|
(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."
|
defaultMessage="An error occurred when trying to evaluate some of the data."
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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>
|
</EuiCallOut>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -218,6 +267,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
<>
|
<>
|
||||||
<EuiSpacer size={'s'} />
|
<EuiSpacer size={'s'} />
|
||||||
<EuiCallOut
|
<EuiCallOut
|
||||||
|
size="s"
|
||||||
title={
|
title={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.infra.metrics.alertFlyout.previewIntervalTooShortTitle"
|
id="xpack.infra.metrics.alertFlyout.previewIntervalTooShortTitle"
|
||||||
|
@ -242,6 +292,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
<EuiSpacer size={'s'} />
|
<EuiSpacer size={'s'} />
|
||||||
{previewError.body?.statusCode === 508 ? (
|
{previewError.body?.statusCode === 508 ? (
|
||||||
<EuiCallOut
|
<EuiCallOut
|
||||||
|
size="s"
|
||||||
title={
|
title={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.infra.metrics.alertFlyout.tooManyBucketsErrorTitle"
|
id="xpack.infra.metrics.alertFlyout.tooManyBucketsErrorTitle"
|
||||||
|
@ -264,6 +315,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
|
||||||
</EuiCallOut>
|
</EuiCallOut>
|
||||||
) : (
|
) : (
|
||||||
<EuiCallOut
|
<EuiCallOut
|
||||||
|
size="s"
|
||||||
title={
|
title={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.infra.metrics.alertFlyout.alertPreviewError"
|
id="xpack.infra.metrics.alertFlyout.alertPreviewError"
|
||||||
|
@ -349,10 +401,3 @@ const previewOptions = [
|
||||||
const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) =>
|
const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) =>
|
||||||
omit(o, 'shortText')
|
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',
|
|
||||||
});
|
|
||||||
|
|
|
@ -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',
|
|
||||||
});
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ describe('Expression', () => {
|
||||||
<Expressions
|
<Expressions
|
||||||
alertsContext={context}
|
alertsContext={context}
|
||||||
alertInterval="1m"
|
alertInterval="1m"
|
||||||
|
alertThrottle="1m"
|
||||||
alertParams={alertParams as any}
|
alertParams={alertParams as any}
|
||||||
errors={[]}
|
errors={[]}
|
||||||
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
|
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
|
||||||
|
|
|
@ -89,6 +89,7 @@ interface Props {
|
||||||
alertOnNoData?: boolean;
|
alertOnNoData?: boolean;
|
||||||
};
|
};
|
||||||
alertInterval: string;
|
alertInterval: string;
|
||||||
|
alertThrottle: string;
|
||||||
alertsContext: AlertsContextValue<AlertContextMeta>;
|
alertsContext: AlertsContextValue<AlertContextMeta>;
|
||||||
setAlertParams(key: string, value: any): void;
|
setAlertParams(key: string, value: any): void;
|
||||||
setAlertProperty(key: string, value: any): void;
|
setAlertProperty(key: string, value: any): void;
|
||||||
|
@ -104,7 +105,14 @@ const defaultExpression = {
|
||||||
} as InventoryMetricConditions;
|
} as InventoryMetricConditions;
|
||||||
|
|
||||||
export const Expressions: React.FC<Props> = (props) => {
|
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({
|
const { source, createDerivedIndexPattern } = useSourceViaHttp({
|
||||||
sourceId: 'default',
|
sourceId: 'default',
|
||||||
type: 'metrics',
|
type: 'metrics',
|
||||||
|
@ -378,6 +386,7 @@ export const Expressions: React.FC<Props> = (props) => {
|
||||||
<EuiSpacer size={'m'} />
|
<EuiSpacer size={'m'} />
|
||||||
<AlertPreview
|
<AlertPreview
|
||||||
alertInterval={alertInterval}
|
alertInterval={alertInterval}
|
||||||
|
alertThrottle={alertThrottle}
|
||||||
alertType={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
|
alertType={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
|
||||||
alertParams={pick(alertParams, 'criteria', 'nodeType', 'sourceId', 'filterQuery')}
|
alertParams={pick(alertParams, 'criteria', 'nodeType', 'sourceId', 'filterQuery')}
|
||||||
validate={validateMetricThreshold}
|
validate={validateMetricThreshold}
|
||||||
|
|
|
@ -68,6 +68,7 @@ describe('Expression', () => {
|
||||||
<Expressions
|
<Expressions
|
||||||
alertsContext={context}
|
alertsContext={context}
|
||||||
alertInterval="1m"
|
alertInterval="1m"
|
||||||
|
alertThrottle="1m"
|
||||||
alertParams={alertParams}
|
alertParams={alertParams}
|
||||||
errors={[]}
|
errors={[]}
|
||||||
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
|
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
|
||||||
|
|
|
@ -51,6 +51,7 @@ interface Props {
|
||||||
alertParams: AlertParams;
|
alertParams: AlertParams;
|
||||||
alertsContext: AlertsContextValue<AlertContextMeta>;
|
alertsContext: AlertsContextValue<AlertContextMeta>;
|
||||||
alertInterval: string;
|
alertInterval: string;
|
||||||
|
alertThrottle: string;
|
||||||
setAlertParams(key: string, value: any): void;
|
setAlertParams(key: string, value: any): void;
|
||||||
setAlertProperty(key: string, value: any): void;
|
setAlertProperty(key: string, value: any): void;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +66,14 @@ const defaultExpression = {
|
||||||
export { defaultExpression };
|
export { defaultExpression };
|
||||||
|
|
||||||
export const Expressions: React.FC<Props> = (props) => {
|
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({
|
const { source, createDerivedIndexPattern } = useSourceViaHttp({
|
||||||
sourceId: 'default',
|
sourceId: 'default',
|
||||||
type: 'metrics',
|
type: 'metrics',
|
||||||
|
@ -399,6 +407,7 @@ export const Expressions: React.FC<Props> = (props) => {
|
||||||
<EuiSpacer size={'m'} />
|
<EuiSpacer size={'m'} />
|
||||||
<AlertPreview
|
<AlertPreview
|
||||||
alertInterval={alertInterval}
|
alertInterval={alertInterval}
|
||||||
|
alertThrottle={alertThrottle}
|
||||||
alertType={METRIC_THRESHOLD_ALERT_TYPE_ID}
|
alertType={METRIC_THRESHOLD_ALERT_TYPE_ID}
|
||||||
alertParams={pick(alertParams, 'criteria', 'groupBy', 'filterQuery', 'sourceId')}
|
alertParams={pick(alertParams, 'criteria', 'groupBy', 'filterQuery', 'sourceId')}
|
||||||
showNoDataResults={alertParams.alertOnNoData}
|
showNoDataResults={alertParams.alertOnNoData}
|
||||||
|
|
|
@ -29,6 +29,8 @@ interface PreviewInventoryMetricThresholdAlertParams {
|
||||||
source: InfraSource;
|
source: InfraSource;
|
||||||
lookback: Unit;
|
lookback: Unit;
|
||||||
alertInterval: string;
|
alertInterval: string;
|
||||||
|
alertThrottle: string;
|
||||||
|
alertOnNoData: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const previewInventoryMetricThresholdAlert = async ({
|
export const previewInventoryMetricThresholdAlert = async ({
|
||||||
|
@ -37,6 +39,8 @@ export const previewInventoryMetricThresholdAlert = async ({
|
||||||
source,
|
source,
|
||||||
lookback,
|
lookback,
|
||||||
alertInterval,
|
alertInterval,
|
||||||
|
alertThrottle,
|
||||||
|
alertOnNoData,
|
||||||
}: PreviewInventoryMetricThresholdAlertParams) => {
|
}: PreviewInventoryMetricThresholdAlertParams) => {
|
||||||
const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams;
|
const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams;
|
||||||
|
|
||||||
|
@ -52,6 +56,10 @@ export const previewInventoryMetricThresholdAlert = async ({
|
||||||
|
|
||||||
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
|
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
|
||||||
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
|
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
|
||||||
|
const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle);
|
||||||
|
const executionsPerThrottle = Math.floor(
|
||||||
|
(throttleIntervalInSeconds / alertIntervalInSeconds) * alertResultsPerExecution
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
criteria.map((c) =>
|
criteria.map((c) =>
|
||||||
|
@ -66,6 +74,12 @@ export const previewInventoryMetricThresholdAlert = async ({
|
||||||
let numberOfTimesFired = 0;
|
let numberOfTimesFired = 0;
|
||||||
let numberOfNoDataResults = 0;
|
let numberOfNoDataResults = 0;
|
||||||
let numberOfErrors = 0;
|
let numberOfErrors = 0;
|
||||||
|
let numberOfNotifications = 0;
|
||||||
|
let throttleTracker = 0;
|
||||||
|
const notifyWithThrottle = () => {
|
||||||
|
if (throttleTracker === 0) numberOfNotifications++;
|
||||||
|
throttleTracker++;
|
||||||
|
};
|
||||||
for (let i = 0; i < numberOfExecutionBuckets; i++) {
|
for (let i = 0; i < numberOfExecutionBuckets; i++) {
|
||||||
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
|
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
|
||||||
const allConditionsFiredInMappedBucket = results.every((result) => {
|
const allConditionsFiredInMappedBucket = results.every((result) => {
|
||||||
|
@ -79,11 +93,27 @@ export const previewInventoryMetricThresholdAlert = async ({
|
||||||
const someConditionsErrorInMappedBucket = results.some((result) => {
|
const someConditionsErrorInMappedBucket = results.some((result) => {
|
||||||
return result[item].isError;
|
return result[item].isError;
|
||||||
});
|
});
|
||||||
if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
|
if (someConditionsErrorInMappedBucket) {
|
||||||
if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
|
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;
|
return previewResults;
|
||||||
|
|
|
@ -16,11 +16,14 @@ describe('Previewing the metric threshold alert type', () => {
|
||||||
...baseParams,
|
...baseParams,
|
||||||
lookback: 'h',
|
lookback: 'h',
|
||||||
alertInterval: '1m',
|
alertInterval: '1m',
|
||||||
|
alertThrottle: '1m',
|
||||||
|
alertOnNoData: true,
|
||||||
});
|
});
|
||||||
const [firedResults, noDataResults, errorResults] = ungroupedResult;
|
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
|
||||||
expect(firedResults).toBe(30);
|
expect(firedResults).toBe(30);
|
||||||
expect(noDataResults).toBe(0);
|
expect(noDataResults).toBe(0);
|
||||||
expect(errorResults).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 () => {
|
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,
|
...baseParams,
|
||||||
lookback: 'h',
|
lookback: 'h',
|
||||||
alertInterval: '3m',
|
alertInterval: '3m',
|
||||||
|
alertThrottle: '3m',
|
||||||
|
alertOnNoData: true,
|
||||||
});
|
});
|
||||||
const [firedResults, noDataResults, errorResults] = ungroupedResult;
|
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
|
||||||
expect(firedResults).toBe(10);
|
expect(firedResults).toBe(10);
|
||||||
expect(noDataResults).toBe(0);
|
expect(noDataResults).toBe(0);
|
||||||
expect(errorResults).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 () => {
|
test('returns the expected results using a bucket interval longer than the alert interval', async () => {
|
||||||
const [ungroupedResult] = await previewMetricThresholdAlert({
|
const [ungroupedResult] = await previewMetricThresholdAlert({
|
||||||
...baseParams,
|
...baseParams,
|
||||||
lookback: 'h',
|
lookback: 'h',
|
||||||
alertInterval: '30s',
|
alertInterval: '30s',
|
||||||
|
alertThrottle: '30s',
|
||||||
|
alertOnNoData: true,
|
||||||
});
|
});
|
||||||
const [firedResults, noDataResults, errorResults] = ungroupedResult;
|
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
|
||||||
expect(firedResults).toBe(60);
|
expect(firedResults).toBe(60);
|
||||||
expect(noDataResults).toBe(0);
|
expect(noDataResults).toBe(0);
|
||||||
expect(errorResults).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', () => {
|
describe('querying with a groupBy parameter', () => {
|
||||||
|
@ -56,15 +79,19 @@ describe('Previewing the metric threshold alert type', () => {
|
||||||
},
|
},
|
||||||
lookback: 'h',
|
lookback: 'h',
|
||||||
alertInterval: '1m',
|
alertInterval: '1m',
|
||||||
|
alertThrottle: '1m',
|
||||||
|
alertOnNoData: true,
|
||||||
});
|
});
|
||||||
const [firedResultsA, noDataResultsA, errorResultsA] = resultA;
|
const [firedResultsA, noDataResultsA, errorResultsA, notificationsA] = resultA;
|
||||||
expect(firedResultsA).toBe(30);
|
expect(firedResultsA).toBe(30);
|
||||||
expect(noDataResultsA).toBe(0);
|
expect(noDataResultsA).toBe(0);
|
||||||
expect(errorResultsA).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(firedResultsB).toBe(60);
|
||||||
expect(noDataResultsB).toBe(0);
|
expect(noDataResultsB).toBe(0);
|
||||||
expect(errorResultsB).toBe(0);
|
expect(errorResultsB).toBe(0);
|
||||||
|
expect(notificationsB).toBe(60);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('querying a data set with a period of No Data', () => {
|
describe('querying a data set with a period of No Data', () => {
|
||||||
|
@ -82,11 +109,14 @@ describe('Previewing the metric threshold alert type', () => {
|
||||||
},
|
},
|
||||||
lookback: 'h',
|
lookback: 'h',
|
||||||
alertInterval: '1m',
|
alertInterval: '1m',
|
||||||
|
alertThrottle: '1m',
|
||||||
|
alertOnNoData: true,
|
||||||
});
|
});
|
||||||
const [firedResults, noDataResults, errorResults] = ungroupedResult;
|
const [firedResults, noDataResults, errorResults, notifications] = ungroupedResult;
|
||||||
expect(firedResults).toBe(25);
|
expect(firedResults).toBe(25);
|
||||||
expect(noDataResults).toBe(10);
|
expect(noDataResults).toBe(10);
|
||||||
expect(errorResults).toBe(0);
|
expect(errorResults).toBe(0);
|
||||||
|
expect(notifications).toBe(35);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,8 @@ interface PreviewMetricThresholdAlertParams {
|
||||||
config: InfraSource['configuration'];
|
config: InfraSource['configuration'];
|
||||||
lookback: Unit;
|
lookback: Unit;
|
||||||
alertInterval: string;
|
alertInterval: string;
|
||||||
|
alertThrottle: string;
|
||||||
|
alertOnNoData: boolean;
|
||||||
end?: number;
|
end?: number;
|
||||||
overrideLookbackIntervalInSeconds?: number;
|
overrideLookbackIntervalInSeconds?: number;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +45,8 @@ export const previewMetricThresholdAlert: (
|
||||||
config,
|
config,
|
||||||
lookback,
|
lookback,
|
||||||
alertInterval,
|
alertInterval,
|
||||||
|
alertThrottle,
|
||||||
|
alertOnNoData,
|
||||||
end = Date.now(),
|
end = Date.now(),
|
||||||
overrideLookbackIntervalInSeconds,
|
overrideLookbackIntervalInSeconds,
|
||||||
},
|
},
|
||||||
|
@ -77,6 +81,11 @@ export const previewMetricThresholdAlert: (
|
||||||
// Now determine how to interpolate this histogram based on the alert interval
|
// Now determine how to interpolate this histogram based on the alert interval
|
||||||
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
|
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
|
||||||
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
|
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
|
||||||
|
const throttleIntervalInSeconds = Math.max(
|
||||||
|
getIntervalInSeconds(alertThrottle),
|
||||||
|
alertIntervalInSeconds
|
||||||
|
);
|
||||||
|
|
||||||
const previewResults = await Promise.all(
|
const previewResults = await Promise.all(
|
||||||
groups.map(async (group) => {
|
groups.map(async (group) => {
|
||||||
// Interpolate the buckets returned by evaluateAlert and return a count of how many of these
|
// 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 numberOfTimesFired = 0;
|
||||||
let numberOfNoDataResults = 0;
|
let numberOfNoDataResults = 0;
|
||||||
let numberOfErrors = 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++) {
|
for (let i = 0; i < numberOfExecutionBuckets; i++) {
|
||||||
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
|
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
|
||||||
const allConditionsFiredInMappedBucket = alertResults.every(
|
const allConditionsFiredInMappedBucket = alertResults.every(
|
||||||
|
@ -102,11 +117,27 @@ export const previewMetricThresholdAlert: (
|
||||||
const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => {
|
const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => {
|
||||||
return alertResult[group].isError;
|
return alertResult[group].isError;
|
||||||
});
|
});
|
||||||
if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
|
if (someConditionsErrorInMappedBucket) {
|
||||||
if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
|
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;
|
return previewResults;
|
||||||
|
@ -114,7 +145,15 @@ export const previewMetricThresholdAlert: (
|
||||||
if (isTooManyBucketsPreviewException(e)) {
|
if (isTooManyBucketsPreviewException(e)) {
|
||||||
// If there's too much data on the first request, recursively slice the lookback interval
|
// If there's too much data on the first request, recursively slice the lookback interval
|
||||||
// until all the data can be retrieved
|
// 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;
|
const { maxBuckets } = e;
|
||||||
// If this is still the first iteration, try to get the number of groups in order to
|
// 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
|
// calculate max buckets. If this fails, just estimate based on 1 group
|
||||||
|
@ -159,7 +198,7 @@ export const previewMetricThresholdAlert: (
|
||||||
.reduce((a, b) => {
|
.reduce((a, b) => {
|
||||||
if (!a) return b;
|
if (!a) return b;
|
||||||
if (!b) return a;
|
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;
|
return zippedResult;
|
||||||
|
|
|
@ -30,7 +30,16 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
framework.router.handleLegacyErrors(async (requestContext, request, response) => {
|
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>) => {
|
const callCluster = (endpoint: string, opts: Record<string, any>) => {
|
||||||
return callWithRequest(requestContext, endpoint, opts);
|
return callWithRequest(requestContext, endpoint, opts);
|
||||||
|
@ -51,22 +60,26 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
||||||
lookback,
|
lookback,
|
||||||
config: source.configuration,
|
config: source.configuration,
|
||||||
alertInterval,
|
alertInterval,
|
||||||
|
alertThrottle,
|
||||||
|
alertOnNoData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const numberOfGroups = previewResult.length;
|
const numberOfGroups = previewResult.length;
|
||||||
const resultTotals = previewResult.reduce(
|
const resultTotals = previewResult.reduce(
|
||||||
(totals, [firedResult, noDataResult, errorResult]) => {
|
(totals, [firedResult, noDataResult, errorResult, notifications]) => {
|
||||||
return {
|
return {
|
||||||
...totals,
|
...totals,
|
||||||
fired: totals.fired + firedResult,
|
fired: totals.fired + firedResult,
|
||||||
noData: totals.noData + noDataResult,
|
noData: totals.noData + noDataResult,
|
||||||
error: totals.error + errorResult,
|
error: totals.error + errorResult,
|
||||||
|
notifications: totals.notifications + notifications,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fired: 0,
|
fired: 0,
|
||||||
noData: 0,
|
noData: 0,
|
||||||
error: 0,
|
error: 0,
|
||||||
|
notifications: 0,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return response.ok({
|
return response.ok({
|
||||||
|
@ -84,22 +97,26 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
|
||||||
lookback,
|
lookback,
|
||||||
source,
|
source,
|
||||||
alertInterval,
|
alertInterval,
|
||||||
|
alertThrottle,
|
||||||
|
alertOnNoData,
|
||||||
});
|
});
|
||||||
|
|
||||||
const numberOfGroups = previewResult.length;
|
const numberOfGroups = previewResult.length;
|
||||||
const resultTotals = previewResult.reduce(
|
const resultTotals = previewResult.reduce(
|
||||||
(totals, [firedResult, noDataResult, errorResult]) => {
|
(totals, [firedResult, noDataResult, errorResult, notifications]) => {
|
||||||
return {
|
return {
|
||||||
...totals,
|
...totals,
|
||||||
fired: totals.fired + firedResult,
|
fired: totals.fired + firedResult,
|
||||||
noData: totals.noData + noDataResult,
|
noData: totals.noData + noDataResult,
|
||||||
error: totals.error + errorResult,
|
error: totals.error + errorResult,
|
||||||
|
notifications: totals.notifications + notifications,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fired: 0,
|
fired: 0,
|
||||||
noData: 0,
|
noData: 0,
|
||||||
error: 0,
|
error: 0,
|
||||||
|
notifications: 0,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -8677,8 +8677,7 @@
|
||||||
"xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "タイミング",
|
"xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "タイミング",
|
||||||
"xpack.infra.metrics.alertFlyout.filterHelpText": "KQL式を使用して、アラートトリガーの範囲を制限します。",
|
"xpack.infra.metrics.alertFlyout.filterHelpText": "KQL式を使用して、アラートトリガーの範囲を制限します。",
|
||||||
"xpack.infra.metrics.alertFlyout.filterLabel": "フィルター(任意)",
|
"xpack.infra.metrics.alertFlyout.filterLabel": "フィルター(任意)",
|
||||||
"xpack.infra.metrics.alertFlyout.firedTime": "時間",
|
"xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 時間} other {# 回数}}",
|
||||||
"xpack.infra.metrics.alertFlyout.firedTimes": "回数",
|
|
||||||
"xpack.infra.metrics.alertFlyout.hourLabel": "時間",
|
"xpack.infra.metrics.alertFlyout.hourLabel": "時間",
|
||||||
"xpack.infra.metrics.alertFlyout.lastDayLabel": "昨日",
|
"xpack.infra.metrics.alertFlyout.lastDayLabel": "昨日",
|
||||||
"xpack.infra.metrics.alertFlyout.lastHourLabel": "過去1時間",
|
"xpack.infra.metrics.alertFlyout.lastHourLabel": "過去1時間",
|
||||||
|
|
|
@ -8683,8 +8683,7 @@
|
||||||
"xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "当",
|
"xpack.infra.metrics.alertFlyout.expression.metric.whenLabel": "当",
|
||||||
"xpack.infra.metrics.alertFlyout.filterHelpText": "使用 KQL 表达式限制告警触发器的范围。",
|
"xpack.infra.metrics.alertFlyout.filterHelpText": "使用 KQL 表达式限制告警触发器的范围。",
|
||||||
"xpack.infra.metrics.alertFlyout.filterLabel": "筛选(可选)",
|
"xpack.infra.metrics.alertFlyout.filterLabel": "筛选(可选)",
|
||||||
"xpack.infra.metrics.alertFlyout.firedTime": "次",
|
"xpack.infra.metrics.alertFlyout.firedTimes": "{fired, plural, one {# 次} other {# 次}}",
|
||||||
"xpack.infra.metrics.alertFlyout.firedTimes": "次",
|
|
||||||
"xpack.infra.metrics.alertFlyout.hourLabel": "小时",
|
"xpack.infra.metrics.alertFlyout.hourLabel": "小时",
|
||||||
"xpack.infra.metrics.alertFlyout.lastDayLabel": "昨天",
|
"xpack.infra.metrics.alertFlyout.lastDayLabel": "昨天",
|
||||||
"xpack.infra.metrics.alertFlyout.lastHourLabel": "上一小时",
|
"xpack.infra.metrics.alertFlyout.lastHourLabel": "上一小时",
|
||||||
|
|
|
@ -248,6 +248,7 @@ export const AlertForm = ({
|
||||||
<AlertParamsExpressionComponent
|
<AlertParamsExpressionComponent
|
||||||
alertParams={alert.params}
|
alertParams={alert.params}
|
||||||
alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`}
|
alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`}
|
||||||
|
alertThrottle={`${alertThrottle ?? 1}${alertThrottleUnit}`}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
setAlertParams={setAlertParams}
|
setAlertParams={setAlertParams}
|
||||||
setAlertProperty={setAlertProperty}
|
setAlertProperty={setAlertProperty}
|
||||||
|
|
|
@ -134,6 +134,7 @@ export interface AlertTypeParamsExpressionProps<
|
||||||
> {
|
> {
|
||||||
alertParams: AlertParamsType;
|
alertParams: AlertParamsType;
|
||||||
alertInterval: string;
|
alertInterval: string;
|
||||||
|
alertThrottle: string;
|
||||||
setAlertParams: (property: string, value: any) => void;
|
setAlertParams: (property: string, value: any) => void;
|
||||||
setAlertProperty: (key: string, value: any) => void;
|
setAlertProperty: (key: string, value: any) => void;
|
||||||
errors: IErrorObject;
|
errors: IErrorObject;
|
||||||
|
|
Loading…
Reference in a new issue