diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 665da25dcbfffd..cdefffeb35c156 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { MetricExpressionParams, Comparator, + Aggregators, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../server/lib/alerting/metric_threshold/types'; import { euiStyled } from '../../../../../observability/public'; @@ -31,6 +32,8 @@ import { // 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 { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; @@ -64,24 +67,25 @@ type MetricExpression = Omit & { metric?: string; }; -enum AGGREGATION_TYPES { - COUNT = 'count', - AVERAGE = 'avg', - SUM = 'sum', - MIN = 'min', - MAX = 'max', - RATE = 'rate', - CARDINALITY = 'cardinality', -} - const defaultExpression = { - aggType: AGGREGATION_TYPES.AVERAGE, + aggType: Aggregators.AVERAGE, comparator: Comparator.GT, threshold: [], timeSize: 1, timeUnit: 'm', } as MetricExpression; +const customComparators = { + ...builtInComparators, + [Comparator.OUTSIDE_RANGE]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', { + defaultMessage: 'Is not between', + }), + value: Comparator.OUTSIDE_RANGE, + requiredValues: 2, + }, +}; + export const Expressions: React.FC = props => { const { setAlertParams, alertParams, errors, alertsContext } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ @@ -339,7 +343,7 @@ const StyledExpression = euiStyled.div` export const ExpressionRow: React.FC = props => { const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props; const { - aggType = AGGREGATION_TYPES.MAX, + aggType = Aggregators.MAX, metric, comparator = Comparator.GT, threshold = [], @@ -410,6 +414,7 @@ export const ExpressionRow: React.FC = props => { { expect(mostRecentAction(instanceID)).toBe(undefined); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); + test('alerts as expected with the outside range comparator', async () => { + await execute(Comparator.OUTSIDE_RANGE, [0, 0.75]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute(Comparator.OUTSIDE_RANGE, [0, 1.5]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); test('reports expected values to the action context', async () => { await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index c5ea65f7a4d1ad..946f1c14bf593a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -11,7 +11,7 @@ import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_ty import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; -import { MetricExpressionParams, Comparator, AlertStates } from './types'; +import { MetricExpressionParams, Comparator, Aggregators, AlertStates } from './types'; import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; @@ -39,7 +39,7 @@ const getCurrentValueFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state const mostRecentBucket = buckets[buckets.length - 1]; - if (aggType === 'count') { + if (aggType === Aggregators.COUNT) { return mostRecentBucket.doc_count; } const { value } = mostRecentBucket.aggregatedValue; @@ -70,10 +70,10 @@ export const getElasticsearchMetricQuery = ( groupBy?: string, filterQuery?: string ) => { - if (aggType === 'count' && metric) { + if (aggType === Aggregators.COUNT && metric) { throw new Error('Cannot aggregate document count with a metric'); } - if (aggType !== 'count' && !metric) { + if (aggType !== Aggregators.COUNT && !metric) { throw new Error('Can only aggregate without a metric if using the document count aggregator'); } const interval = `${timeSize}${timeUnit}`; @@ -85,9 +85,9 @@ export const getElasticsearchMetricQuery = ( const offset = getDateHistogramOffset(from, interval); const aggregations = - aggType === 'count' + aggType === Aggregators.COUNT ? {} - : aggType === 'rate' + : aggType === Aggregators.RATE ? networkTraffic('aggregatedValue', metric) : { aggregatedValue: { @@ -242,7 +242,8 @@ const getMetric: ( const comparatorMap = { [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => value >= Math.min(a, b) && value <= Math.max(a, b), - // `threshold` is always an array of numbers in case the BETWEEN comparator is + [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b, + // `threshold` is always an array of numbers in case the BETWEEN/OUTSIDE_RANGE comparator is // used; all other compartors will just destructure the first value in the array [Comparator.GT]: (a: number, [b]: number[]) => a > b, [Comparator.LT]: (a: number, [b]: number[]) => a < b, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 8808219cabaa70..b697af4fa4c3b2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -7,8 +7,15 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { PluginSetupContract } from '../../../../../alerting/server'; +import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; + +const oneOfLiterals = (arrayOfLiterals: Readonly) => + schema.string({ + validate: value => + arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`, + }); export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { if (!alertingPlugin) { @@ -20,13 +27,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet const baseCriterion = { threshold: schema.arrayOf(schema.number()), - comparator: schema.oneOf([ - schema.literal('>'), - schema.literal('<'), - schema.literal('>='), - schema.literal('<='), - schema.literal('between'), - ]), + comparator: oneOfLiterals(Object.values(Comparator)), timeUnit: schema.string(), timeSize: schema.number(), }; @@ -34,13 +35,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet const nonCountCriterion = schema.object({ ...baseCriterion, metric: schema.string(), - aggType: schema.oneOf([ - schema.literal('avg'), - schema.literal('min'), - schema.literal('max'), - schema.literal('rate'), - schema.literal('cardinality'), - ]), + aggType: oneOfLiterals(METRIC_EXPLORER_AGGREGATIONS), }); const countCriterion = schema.object({ diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index abed691f109c00..18f5503fe2c9e3 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerAggregation } from '../../../../common/http_api/metrics_explorer'; - export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export enum Comparator { @@ -14,6 +12,17 @@ export enum Comparator { GT_OR_EQ = '>=', LT_OR_EQ = '<=', BETWEEN = 'between', + OUTSIDE_RANGE = 'outside', +} + +export enum Aggregators { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', + RATE = 'rate', + CARDINALITY = 'cardinality', } export enum AlertStates { @@ -34,7 +43,7 @@ interface BaseMetricExpressionParams { } interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { - aggType: Exclude; + aggType: Exclude; metric: string; }