[Metrics UI] Fix alert preview accuracy with new Notify settings (#89939)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Zacqary Adam Xeper 2021-02-11 14:04:03 -06:00 committed by GitHub
parent 1fbea8cd78
commit 15277e187c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 237 additions and 101 deletions

View file

@ -75,6 +75,7 @@ const baseAlertRequestParamsRT = rt.intersection([
alertInterval: rt.string,
alertThrottle: rt.string,
alertOnNoData: rt.boolean,
alertNotifyWhen: rt.string,
}),
]);

View file

@ -21,6 +21,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { AlertNotifyWhenType } from '../../../../../alerts/common';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { FORMATTERS } from '../../../../common/formatters';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@ -36,6 +37,7 @@ import { getAlertPreview, PreviewableAlertTypes } from './get_alert_preview';
interface Props {
alertInterval: string;
alertThrottle: string;
alertNotifyWhen: AlertNotifyWhenType;
alertType: PreviewableAlertTypes;
alertParams: { criteria?: any[]; sourceId: string } & Record<string, any>;
validate: (params: any) => ValidationResult;
@ -48,6 +50,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
alertParams,
alertInterval,
alertThrottle,
alertNotifyWhen,
alertType,
validate,
showNoDataResults,
@ -78,6 +81,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
alertInterval,
alertThrottle,
alertNotifyWhen,
alertOnNoData: showNoDataResults ?? false,
} as AlertPreviewRequestParams,
alertType,
@ -92,6 +96,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
alertParams,
alertInterval,
alertType,
alertNotifyWhen,
groupByDisplayName,
previewLookbackInterval,
alertThrottle,
@ -119,10 +124,11 @@ export const AlertPreview: React.FC<Props> = (props) => {
const showNumberOfNotifications = useMemo(() => {
if (!previewResult) return false;
if (alertNotifyWhen === 'onActiveAlert') return false;
const { notifications, fired, noData, error } = previewResult.resultTotals;
const unthrottledNotifications = fired + (showNoDataResults ? noData + error : 0);
return unthrottledNotifications > notifications;
}, [previewResult, showNoDataResults]);
}, [previewResult, showNoDataResults, alertNotifyWhen]);
const hasWarningThreshold = useMemo(
() => alertParams.criteria?.some((c) => Reflect.has(c, 'warningThreshold')) ?? false,
@ -213,9 +219,17 @@ export const AlertPreview: React.FC<Props> = (props) => {
<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}."'
defaultMessage='As a result, this alert would have sent {notifications} based on the selected "notify" setting of "{alertThrottle}."'
values={{
alertThrottle: previewResult.alertThrottle,
alertThrottle:
alertNotifyWhen === 'onThrottleInterval'
? previewResult.alertThrottle
: i18n.translate(
'xpack.infra.metrics.alertFlyout.alertPreviewOnlyOnStatusChange',
{
defaultMessage: 'Only on status change',
}
),
notifications: (
<strong>
{i18n.translate(

View file

@ -48,8 +48,9 @@ describe('Expression', () => {
<Expressions
alertInterval="1m"
alertThrottle="1m"
alertNotifyWhen="onThrottleInterval"
alertParams={alertParams as any}
errors={[]}
errors={{}}
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
setAlertProperty={() => {}}
metadata={currentOptions}

View file

@ -38,8 +38,10 @@ import {
ForLastExpression,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../triggers_actions_ui/public/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
import {
IErrorObject,
AlertTypeParamsExpressionProps,
} from '../../../../../triggers_actions_ui/public';
import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items';
@ -78,22 +80,21 @@ export interface AlertContextMeta {
customMetrics?: SnapshotCustomMetricInput[];
}
interface Props {
errors: IErrorObject[];
alertParams: {
criteria: InventoryMetricConditions[];
nodeType: InventoryItemType;
filterQuery?: string;
filterQueryText?: string;
sourceId: string;
alertOnNoData?: boolean;
};
alertInterval: string;
alertThrottle: string;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
metadata: AlertContextMeta;
}
type Criteria = InventoryMetricConditions[];
type Props = Omit<
AlertTypeParamsExpressionProps<
{
criteria: Criteria;
nodeType: InventoryItemType;
filterQuery?: string;
filterQueryText?: string;
sourceId: string;
alertOnNoData?: boolean;
},
AlertContextMeta
>,
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data'
>;
export const defaultExpression = {
metric: 'cpu' as SnapshotMetricType,
@ -111,7 +112,15 @@ export const defaultExpression = {
export const Expressions: React.FC<Props> = (props) => {
const { http, notifications } = useKibanaContextForPlugin().services;
const { setAlertParams, alertParams, errors, alertInterval, alertThrottle, metadata } = props;
const {
setAlertParams,
alertParams,
errors,
alertInterval,
alertThrottle,
metadata,
alertNotifyWhen,
} = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
@ -186,7 +195,7 @@ export const Expressions: React.FC<Props> = (props) => {
timeSize: ts,
}));
setTimeSize(ts || undefined);
setAlertParams('criteria', criteria);
setAlertParams('criteria', criteria as Criteria);
},
[alertParams.criteria, setAlertParams]
);
@ -198,7 +207,7 @@ export const Expressions: React.FC<Props> = (props) => {
timeUnit: tu,
}));
setTimeUnit(tu as Unit);
setAlertParams('criteria', criteria);
setAlertParams('criteria', criteria as Criteria);
},
[alertParams.criteria, setAlertParams]
);
@ -301,7 +310,7 @@ export const Expressions: React.FC<Props> = (props) => {
key={idx} // idx's don't usually make good key's but here the index has semantic meaning
expressionId={idx}
setAlertParams={updateParams}
errors={errors[idx] || emptyError}
errors={(errors[idx] as IErrorObject) || emptyError}
expression={e || {}}
fields={derivedIndexPattern.fields}
/>
@ -385,6 +394,7 @@ export const Expressions: React.FC<Props> = (props) => {
<AlertPreview
alertInterval={alertInterval}
alertThrottle={alertThrottle}
alertNotifyWhen={alertNotifyWhen}
alertType={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID}
alertParams={pick(alertParams, 'criteria', 'nodeType', 'sourceId', 'filterQuery')}
validate={validateMetricThreshold}
@ -406,7 +416,7 @@ interface ExpressionRowProps {
expression: Omit<InventoryMetricConditions, 'metric'> & {
metric?: SnapshotMetricType;
};
errors: IErrorObject;
errors: AlertTypeParamsExpressionProps['errors'];
canDelete: boolean;
addExpression(): void;
remove(id: number): void;

View file

@ -43,8 +43,9 @@ describe('Expression', () => {
<Expression
alertInterval="1m"
alertThrottle="1m"
alertNotifyWhen="onThrottleInterval"
alertParams={alertParams as any}
errors={[]}
errors={{}}
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
setAlertProperty={() => {}}
metadata={currentOptions}

View file

@ -22,8 +22,11 @@ import {
WhenExpression,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../triggers_actions_ui/public/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
import {
AlertTypeParams,
AlertTypeParamsExpressionProps,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../triggers_actions_ui/public/types';
import { useSourceViaHttp } from '../../../containers/source/use_source_via_http';
import { findInventoryModel } from '../../../../common/inventory_models';
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
@ -41,29 +44,32 @@ export interface AlertContextMeta {
nodeType?: InventoryItemType;
}
interface Props {
errors: IErrorObject[];
alertParams: MetricAnomalyParams & {
sourceId: string;
};
alertInterval: string;
alertThrottle: string;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
metadata: AlertContextMeta;
}
type AlertParams = AlertTypeParams &
MetricAnomalyParams & { sourceId: string; hasInfraMLCapabilities: boolean };
type Props = Omit<
AlertTypeParamsExpressionProps<AlertParams, AlertContextMeta>,
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data'
>;
export const defaultExpression = {
metric: 'memory_usage' as MetricAnomalyParams['metric'],
threshold: ANOMALY_THRESHOLD.MAJOR,
nodeType: 'hosts',
threshold: ANOMALY_THRESHOLD.MAJOR as MetricAnomalyParams['threshold'],
nodeType: 'hosts' as MetricAnomalyParams['nodeType'],
influencerFilter: undefined,
};
export const Expression: React.FC<Props> = (props) => {
const { hasInfraMLCapabilities, isLoading: isLoadingMLCapabilities } = useInfraMLCapabilities();
const { http, notifications } = useKibanaContextForPlugin().services;
const { setAlertParams, alertParams, alertInterval, alertThrottle, metadata } = props;
const {
setAlertParams,
alertParams,
alertInterval,
alertThrottle,
alertNotifyWhen,
metadata,
} = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
@ -97,7 +103,7 @@ export const Expression: React.FC<Props> = (props) => {
setAlertParams('influencerFilter', {
...alertParams.influencerFilter,
fieldValue: value,
});
} as MetricAnomalyParams['influencerFilter']);
} else {
setAlertParams('influencerFilter', undefined);
}
@ -118,7 +124,7 @@ export const Expression: React.FC<Props> = (props) => {
const updateMetric = useCallback(
(metric: string) => {
setAlertParams('metric', metric);
setAlertParams('metric', metric as MetricAnomalyParams['metric']);
},
[setAlertParams]
);
@ -249,6 +255,7 @@ export const Expression: React.FC<Props> = (props) => {
<AlertPreview
alertInterval={alertInterval}
alertThrottle={alertThrottle}
alertNotifyWhen={alertNotifyWhen}
alertType={METRIC_ANOMALY_ALERT_TYPE_ID}
alertParams={pick(
alertParams,
@ -295,7 +302,9 @@ export const nodeTypes: { [key: string]: any } = {
},
};
const getMLMetricFromInventoryMetric = (metric: SnapshotMetricType) => {
const getMLMetricFromInventoryMetric: (
metric: SnapshotMetricType
) => MetricAnomalyParams['metric'] | null = (metric) => {
switch (metric) {
case 'memory':
return 'memory_usage';
@ -308,7 +317,9 @@ const getMLMetricFromInventoryMetric = (metric: SnapshotMetricType) => {
}
};
const getMLNodeTypeFromInventoryNodeType = (nodeType: InventoryItemType) => {
const getMLNodeTypeFromInventoryNodeType: (
nodeType: InventoryItemType
) => MetricAnomalyParams['nodeType'] | null = (nodeType) => {
switch (nodeType) {
case 'host':
return 'hosts';

View file

@ -44,8 +44,9 @@ describe('Expression', () => {
<Expressions
alertInterval="1m"
alertThrottle="1m"
alertNotifyWhen="onThrottleInterval"
alertParams={alertParams}
errors={[]}
errors={{}}
setAlertParams={(key, value) => Reflect.set(alertParams, key, value)}
setAlertProperty={() => {}}
metadata={{

View file

@ -30,8 +30,12 @@ import {
ForLastExpression,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../triggers_actions_ui/public/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { IErrorObject } from '../../../../../triggers_actions_ui/public/types';
import {
IErrorObject,
AlertTypeParams,
AlertTypeParamsExpressionProps,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../triggers_actions_ui/public/types';
import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar';
import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by';
@ -46,15 +50,10 @@ import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
const FILTER_TYPING_DEBOUNCE_MS = 500;
interface Props {
errors: IErrorObject[];
alertParams: AlertParams;
alertInterval: string;
alertThrottle: string;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
metadata: AlertContextMeta;
}
type Props = Omit<
AlertTypeParamsExpressionProps<AlertTypeParams & AlertParams, AlertContextMeta>,
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data'
>;
const defaultExpression = {
aggType: Aggregators.AVERAGE,
@ -66,7 +65,15 @@ const defaultExpression = {
export { defaultExpression };
export const Expressions: React.FC<Props> = (props) => {
const { setAlertParams, alertParams, errors, alertInterval, alertThrottle, metadata } = props;
const {
setAlertParams,
alertParams,
errors,
alertInterval,
alertThrottle,
metadata,
alertNotifyWhen,
} = props;
const { http, notifications } = useKibanaContextForPlugin().services;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
@ -76,7 +83,7 @@ export const Expressions: React.FC<Props> = (props) => {
});
const [timeSize, setTimeSize] = useState<number | undefined>(1);
const [timeUnit, setTimeUnit] = useState<Unit>('m');
const [timeUnit, setTimeUnit] = useState<Unit | undefined>('m');
const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
createDerivedIndexPattern,
]);
@ -174,7 +181,7 @@ export const Expressions: React.FC<Props> = (props) => {
timeUnit: tu,
})) || [];
setTimeUnit(tu as Unit);
setAlertParams('criteria', criteria);
setAlertParams('criteria', criteria as AlertParams['criteria']);
},
[alertParams.criteria, setAlertParams]
);
@ -191,7 +198,7 @@ export const Expressions: React.FC<Props> = (props) => {
timeSize,
timeUnit,
aggType: metric.aggregation,
}))
})) as AlertParams['criteria']
);
} else {
setAlertParams('criteria', [defaultExpression]);
@ -280,7 +287,7 @@ export const Expressions: React.FC<Props> = (props) => {
key={idx} // idx's don't usually make good key's but here the index has semantic meaning
expressionId={idx}
setAlertParams={updateParams}
errors={errors[idx] || emptyError}
errors={(errors[idx] as IErrorObject) || emptyError}
expression={e || {}}
>
<ExpressionChart
@ -396,6 +403,7 @@ export const Expressions: React.FC<Props> = (props) => {
<AlertPreview
alertInterval={alertInterval}
alertThrottle={alertThrottle}
alertNotifyWhen={alertNotifyWhen}
alertType={METRIC_THRESHOLD_ALERT_TYPE_ID}
alertParams={pick(alertParams, 'criteria', 'groupBy', 'filterQuery', 'sourceId')}
showNoDataResults={alertParams.alertOnNoData}

View file

@ -328,7 +328,7 @@ export const ExpressionChart: React.FC<Props> = ({
<FormattedMessage
id="xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping"
defaultMessage="Last {lookback} {timeLabel} of data for {id}"
values={{ id: series.id, timeLabel, lookback: timeSize * 20 }}
values={{ id: series.id, timeLabel, lookback: timeSize! * 20 }}
/>
</EuiText>
) : (
@ -336,7 +336,7 @@ export const ExpressionChart: React.FC<Props> = ({
<FormattedMessage
id="xpack.infra.metrics.alerts.dataTimeRangeLabel"
defaultMessage="Last {lookback} {timeLabel}"
values={{ timeLabel, lookback: timeSize * 20 }}
values={{ timeLabel, lookback: timeSize! * 20 }}
/>
</EuiText>
)}

View file

@ -17,8 +17,10 @@ export interface AlertContextMeta {
series?: MetricsExplorerSeries;
}
export type MetricExpression = Omit<MetricExpressionParams, 'metric'> & {
metric?: string;
export type MetricExpression = Omit<MetricExpressionParams, 'metric' | 'timeSize' | 'timeUnit'> & {
metric?: MetricExpressionParams['metric'];
timeSize?: MetricExpressionParams['timeSize'];
timeUnit?: MetricExpressionParams['timeUnit'];
};
export enum AGGREGATION_TYPES {
@ -54,7 +56,7 @@ export interface ExpressionChartData {
export interface AlertParams {
criteria: MetricExpression[];
groupBy?: string[];
groupBy?: string | string[];
filterQuery?: string;
sourceId: string;
filterQueryText?: string;

View file

@ -34,6 +34,7 @@ interface PreviewInventoryMetricThresholdAlertParams {
alertInterval: string;
alertThrottle: string;
alertOnNoData: boolean;
alertNotifyWhen: string;
}
export const previewInventoryMetricThresholdAlert: (
@ -46,7 +47,8 @@ export const previewInventoryMetricThresholdAlert: (
alertInterval,
alertThrottle,
alertOnNoData,
}) => {
alertNotifyWhen,
}: PreviewInventoryMetricThresholdAlertParams) => {
const { criteria, filterQuery, nodeType } = params as InventoryMetricThresholdParams;
if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions');
@ -62,9 +64,7 @@ export const previewInventoryMetricThresholdAlert: (
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) =>
@ -82,9 +82,17 @@ export const previewInventoryMetricThresholdAlert: (
let numberOfErrors = 0;
let numberOfNotifications = 0;
let throttleTracker = 0;
const notifyWithThrottle = () => {
if (throttleTracker === 0) numberOfNotifications++;
throttleTracker++;
let previousActionGroup: string | null = null;
const notifyWithThrottle = (actionGroup: string) => {
if (alertNotifyWhen === 'onActionGroupChange') {
if (previousActionGroup !== actionGroup) numberOfNotifications++;
} else if (alertNotifyWhen === 'onThrottleInterval') {
if (throttleTracker === 0) numberOfNotifications++;
throttleTracker += alertIntervalInSeconds;
} else {
numberOfNotifications++;
}
previousActionGroup = actionGroup;
};
for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
@ -105,23 +113,26 @@ export const previewInventoryMetricThresholdAlert: (
if (someConditionsErrorInMappedBucket) {
numberOfErrors++;
if (alertOnNoData) {
notifyWithThrottle();
notifyWithThrottle('fired'); // TODO: Update this when No Data alerts move to an action group
}
} else if (someConditionsNoDataInMappedBucket) {
numberOfNoDataResults++;
if (alertOnNoData) {
notifyWithThrottle();
notifyWithThrottle('fired'); // TODO: Update this when No Data alerts move to an action group
}
} else if (allConditionsFiredInMappedBucket) {
numberOfTimesFired++;
notifyWithThrottle();
notifyWithThrottle('fired');
} else if (allConditionsWarnInMappedBucket) {
numberOfTimesWarned++;
notifyWithThrottle();
} else if (throttleTracker > 0) {
throttleTracker++;
notifyWithThrottle('warning');
} else {
previousActionGroup = 'recovered';
if (throttleTracker > 0) {
throttleTracker += alertIntervalInSeconds;
}
}
if (throttleTracker === executionsPerThrottle) {
if (throttleTracker >= throttleIntervalInSeconds) {
throttleTracker = 0;
}
}

View file

@ -27,6 +27,7 @@ interface PreviewMetricAnomalyAlertParams {
alertInterval: string;
alertThrottle: string;
alertOnNoData: boolean;
alertNotifyWhen: string;
}
export const previewMetricAnomalyAlert = async ({
@ -38,12 +39,12 @@ export const previewMetricAnomalyAlert = async ({
lookback,
alertInterval,
alertThrottle,
alertNotifyWhen,
}: PreviewMetricAnomalyAlertParams) => {
const { metric, threshold, influencerFilter, nodeType } = params as MetricAnomalyParams;
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
const throttleIntervalInSeconds = getIntervalInSeconds(alertThrottle);
const executionsPerThrottle = Math.floor(throttleIntervalInSeconds / alertIntervalInSeconds);
const lookbackInterval = `1${lookback}`;
const lookbackIntervalInSeconds = getIntervalInSeconds(lookbackInterval);
@ -78,9 +79,17 @@ export const previewMetricAnomalyAlert = async ({
let numberOfTimesFired = 0;
let numberOfNotifications = 0;
let throttleTracker = 0;
const notifyWithThrottle = () => {
if (throttleTracker === 0) numberOfNotifications++;
throttleTracker++;
let previousActionGroup: string | null = null;
const notifyWithThrottle = (actionGroup: string) => {
if (alertNotifyWhen === 'onActionGroupChange') {
if (previousActionGroup !== actionGroup) numberOfNotifications++;
} else if (alertNotifyWhen === 'onThrottleInterval') {
if (throttleTracker === 0) numberOfNotifications++;
throttleTracker += alertIntervalInSeconds;
} else {
numberOfNotifications++;
}
previousActionGroup = actionGroup;
};
// Mock each alert evaluation
for (let i = 0; i < numberOfExecutions; i++) {
@ -102,11 +111,14 @@ export const previewMetricAnomalyAlert = async ({
if (anomaliesDetectedInBuckets) {
numberOfTimesFired++;
notifyWithThrottle();
} else if (throttleTracker > 0) {
throttleTracker++;
notifyWithThrottle('fired');
} else {
previousActionGroup = 'recovered';
if (throttleTracker > 0) {
throttleTracker += alertIntervalInSeconds;
}
}
if (throttleTracker === executionsPerThrottle) {
if (throttleTracker >= throttleIntervalInSeconds) {
throttleTracker = 0;
}
}

View file

@ -19,6 +19,7 @@ describe('Previewing the metric threshold alert type', () => {
alertInterval: '1m',
alertThrottle: '1m',
alertOnNoData: true,
alertNotifyWhen: 'onThrottleInterval',
});
const { fired, noData, error, notifications } = ungroupedResult;
expect(fired).toBe(30);
@ -34,6 +35,7 @@ describe('Previewing the metric threshold alert type', () => {
alertInterval: '3m',
alertThrottle: '3m',
alertOnNoData: true,
alertNotifyWhen: 'onThrottleInterval',
});
const { fired, noData, error, notifications } = ungroupedResult;
expect(fired).toBe(10);
@ -48,6 +50,7 @@ describe('Previewing the metric threshold alert type', () => {
alertInterval: '30s',
alertThrottle: '30s',
alertOnNoData: true,
alertNotifyWhen: 'onThrottleInterval',
});
const { fired, noData, error, notifications } = ungroupedResult;
expect(fired).toBe(60);
@ -62,6 +65,7 @@ describe('Previewing the metric threshold alert type', () => {
alertInterval: '1m',
alertThrottle: '3m',
alertOnNoData: true,
alertNotifyWhen: 'onThrottleInterval',
});
const { fired, noData, error, notifications } = ungroupedResult;
expect(fired).toBe(30);
@ -69,6 +73,30 @@ describe('Previewing the metric threshold alert type', () => {
expect(error).toBe(0);
expect(notifications).toBe(15);
});
test('returns the expected results using a notify setting of Only on Status Change', async () => {
const [ungroupedResult] = await previewMetricThresholdAlert({
...baseParams,
params: {
...baseParams.params,
criteria: [
{
...baseCriterion,
metric: 'test.metric.3',
} as MetricExpressionParams,
],
},
lookback: 'h',
alertInterval: '1m',
alertThrottle: '1m',
alertOnNoData: true,
alertNotifyWhen: 'onActionGroupChange',
});
const { fired, noData, error, notifications } = ungroupedResult;
expect(fired).toBe(20);
expect(noData).toBe(0);
expect(error).toBe(0);
expect(notifications).toBe(20);
});
});
describe('querying with a groupBy parameter', () => {
test('returns the expected results', async () => {
@ -82,6 +110,7 @@ describe('Previewing the metric threshold alert type', () => {
alertInterval: '1m',
alertThrottle: '1m',
alertOnNoData: true,
alertNotifyWhen: 'onThrottleInterval',
});
const {
fired: firedA,
@ -122,6 +151,7 @@ describe('Previewing the metric threshold alert type', () => {
alertInterval: '1m',
alertThrottle: '1m',
alertOnNoData: true,
alertNotifyWhen: 'onThrottleInterval',
});
const { fired, noData, error, notifications } = ungroupedResult;
expect(fired).toBe(25);
@ -144,6 +174,9 @@ services.callCluster.mockImplementation(async (_: string, { body, index }: any)
if (metric === 'test.metric.2') {
return mocks.alternateMetricPreviewResponse;
}
if (metric === 'test.metric.3') {
return mocks.repeatingMetricPreviewResponse;
}
return mocks.basicMetricPreviewResponse;
});

View file

@ -31,6 +31,7 @@ interface PreviewMetricThresholdAlertParams {
lookback: Unit;
alertInterval: string;
alertThrottle: string;
alertNotifyWhen: string;
alertOnNoData: boolean;
end?: number;
overrideLookbackIntervalInSeconds?: number;
@ -48,6 +49,7 @@ export const previewMetricThresholdAlert: (
lookback,
alertInterval,
alertThrottle,
alertNotifyWhen,
alertOnNoData,
end = Date.now(),
overrideLookbackIntervalInSeconds,
@ -104,9 +106,17 @@ export const previewMetricThresholdAlert: (
let numberOfErrors = 0;
let numberOfNotifications = 0;
let throttleTracker = 0;
const notifyWithThrottle = () => {
if (throttleTracker === 0) numberOfNotifications++;
throttleTracker += alertIntervalInSeconds;
let previousActionGroup: string | null = null;
const notifyWithThrottle = (actionGroup: string) => {
if (alertNotifyWhen === 'onActionGroupChange') {
if (previousActionGroup !== actionGroup) numberOfNotifications++;
previousActionGroup = actionGroup;
} else if (alertNotifyWhen === 'onThrottleInterval') {
if (throttleTracker === 0) numberOfNotifications++;
throttleTracker += alertIntervalInSeconds;
} else {
numberOfNotifications++;
}
};
for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
@ -126,21 +136,24 @@ export const previewMetricThresholdAlert: (
if (someConditionsErrorInMappedBucket) {
numberOfErrors++;
if (alertOnNoData) {
notifyWithThrottle();
notifyWithThrottle('fired'); // TODO: Update this when No Data alerts move to an action group
}
} else if (someConditionsNoDataInMappedBucket) {
numberOfNoDataResults++;
if (alertOnNoData) {
notifyWithThrottle();
notifyWithThrottle('fired'); // TODO: Update this when No Data alerts move to an action group
}
} else if (allConditionsFiredInMappedBucket) {
numberOfTimesFired++;
notifyWithThrottle();
notifyWithThrottle('fired');
} else if (allConditionsWarnInMappedBucket) {
numberOfTimesWarned++;
notifyWithThrottle();
} else if (throttleTracker > 0) {
throttleTracker += alertIntervalInSeconds;
notifyWithThrottle('warning');
} else {
previousActionGroup = 'recovered';
if (throttleTracker > 0) {
throttleTracker += alertIntervalInSeconds;
}
}
if (throttleTracker >= throttleIntervalInSeconds) {
throttleTracker = 0;
@ -168,6 +181,7 @@ export const previewMetricThresholdAlert: (
alertInterval,
alertThrottle,
alertOnNoData,
alertNotifyWhen,
};
const { maxBuckets } = e;
// If this is still the first iteration, try to get the number of groups in order to

View file

@ -45,6 +45,7 @@ const previewBucketsWithNulls = [
...Array.from(Array(10), (_, i) => ({ aggregatedValue: { value: null } })),
...previewBucketsA.slice(10),
];
const previewBucketsRepeat = Array.from(Array(60), (_, i) => bucketsA[Math.max(0, (i % 3) - 1)]);
export const basicMetricResponse = {
aggregations: {
@ -175,6 +176,14 @@ export const alternateMetricPreviewResponse = {
},
};
export const repeatingMetricPreviewResponse = {
aggregations: {
aggregatedIntervals: {
buckets: previewBucketsRepeat,
},
},
};
export const basicCompositePreviewResponse = {
aggregations: {
groupings: {

View file

@ -43,6 +43,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
alertInterval,
alertThrottle,
alertOnNoData,
alertNotifyWhen,
} = request.body;
const callCluster = (endpoint: string, opts: Record<string, any>) => {
@ -69,6 +70,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
config: source.configuration,
alertInterval,
alertThrottle,
alertNotifyWhen,
alertOnNoData,
});
@ -90,6 +92,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
source,
alertInterval,
alertThrottle,
alertNotifyWhen,
alertOnNoData,
});
@ -119,6 +122,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
alertInterval,
alertThrottle,
alertOnNoData,
alertNotifyWhen,
});
return response.ok({

View file

@ -147,6 +147,7 @@ describe('EsQueryAlertTypeExpression', () => {
<EsQueryAlertTypeExpression
alertInterval="1m"
alertThrottle="1m"
alertNotifyWhen="onThrottleInterval"
alertParams={alertParams}
setAlertParams={() => {}}
setAlertProperty={() => {}}

View file

@ -89,6 +89,7 @@ describe('IndexThresholdAlertTypeExpression', () => {
<IndexThresholdAlertTypeExpression
alertInterval="1m"
alertThrottle="1m"
alertNotifyWhen="onThrottleInterval"
alertParams={alertParams}
setAlertParams={() => {}}
setAlertProperty={() => {}}

View file

@ -591,6 +591,7 @@ export const AlertForm = ({
alertParams={alert.params}
alertInterval={`${alertInterval ?? 1}${alertIntervalUnit}`}
alertThrottle={`${alertThrottle ?? 1}${alertThrottleUnit}`}
alertNotifyWhen={alert.notifyWhen ?? 'onActionGroupChange'}
errors={errors}
setAlertParams={setAlertParams}
setAlertProperty={setAlertProperty}

View file

@ -198,6 +198,7 @@ export interface AlertTypeParamsExpressionProps<
alertParams: Params;
alertInterval: string;
alertThrottle: string;
alertNotifyWhen: AlertNotifyWhenType;
setAlertParams: <Key extends keyof Params>(property: Key, value: Params[Key] | undefined) => void;
setAlertProperty: <Prop extends keyof Alert>(
key: Prop,