diff --git a/x-pack/plugins/infra/common/alerting/logs/types.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts similarity index 69% rename from x-pack/plugins/infra/common/alerting/logs/types.ts rename to x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts index 1b736f52aa7e..c505a234c7b2 100644 --- a/x-pack/plugins/infra/common/alerting/logs/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts @@ -5,10 +5,18 @@ */ import { i18n } from '@kbn/i18n'; import * as rt from 'io-ts'; -import { commonSearchSuccessResponseFieldsRT } from '../../utils/elasticsearch_runtime_types'; +import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; export const LOG_DOCUMENT_COUNT_ALERT_TYPE_ID = 'logs.alert.document.count'; +const ThresholdTypeRT = rt.keyof({ + count: null, + ratio: null, +}); + +export type ThresholdType = rt.TypeOf; + +// Comparators // export enum Comparator { GT = 'more than', GT_OR_EQ = 'more than or equals', @@ -82,6 +90,7 @@ export const ComparatorToi18nMap = { ), }; +// Alert parameters // export enum AlertStates { OK, ALERT, @@ -89,12 +98,12 @@ export enum AlertStates { ERROR, } -const DocumentCountRT = rt.type({ +const ThresholdRT = rt.type({ comparator: ComparatorRT, value: rt.number, }); -export type DocumentCount = rt.TypeOf; +export type Threshold = rt.TypeOf; export const CriterionRT = rt.type({ field: rt.string, @@ -104,6 +113,13 @@ export const CriterionRT = rt.type({ export type Criterion = rt.TypeOf; export const criteriaRT = rt.array(CriterionRT); +export type Criteria = rt.TypeOf; + +export const countCriteriaRT = criteriaRT; +export type CountCriteria = rt.TypeOf; + +export const ratioCriteriaRT = rt.tuple([criteriaRT, criteriaRT]); +export type RatioCriteria = rt.TypeOf; export const TimeUnitRT = rt.union([ rt.literal('s'), @@ -116,25 +132,73 @@ export type TimeUnit = rt.TypeOf; export const timeSizeRT = rt.number; export const groupByRT = rt.array(rt.string); -export const LogDocumentCountAlertParamsRT = rt.intersection([ +const RequiredAlertParamsRT = rt.type({ + // NOTE: "count" would be better named as "threshold", but this would require a + // migration of encrypted saved objects, so we'll keep "count" until it's problematic. + count: ThresholdRT, + timeUnit: TimeUnitRT, + timeSize: timeSizeRT, +}); + +const OptionalAlertParamsRT = rt.partial({ + groupBy: groupByRT, +}); + +export const alertParamsRT = rt.intersection([ rt.type({ - count: DocumentCountRT, - criteria: criteriaRT, - timeUnit: TimeUnitRT, - timeSize: timeSizeRT, + criteria: countCriteriaRT, + ...RequiredAlertParamsRT.props, }), rt.partial({ - groupBy: groupByRT, + ...OptionalAlertParamsRT.props, }), ]); -export type LogDocumentCountAlertParams = rt.TypeOf; +export type CountAlertParams = rt.TypeOf; +export const ratioAlertParamsRT = rt.intersection([ + rt.type({ + criteria: ratioCriteriaRT, + ...RequiredAlertParamsRT.props, + }), + rt.partial({ + ...OptionalAlertParamsRT.props, + }), +]); + +export type RatioAlertParams = rt.TypeOf; + +export const AlertParamsRT = rt.union([alertParamsRT, ratioAlertParamsRT]); +export type AlertParams = rt.TypeOf; + +export const isRatioAlert = (criteria: AlertParams['criteria']): criteria is RatioCriteria => { + return criteria.length > 0 && Array.isArray(criteria[0]) ? true : false; +}; + +export const isRatioAlertParams = (params: AlertParams): params is RatioAlertParams => { + return isRatioAlert(params.criteria); +}; + +export const getNumerator = (criteria: RatioCriteria): Criteria => { + return criteria[0]; +}; + +export const getDenominator = (criteria: RatioCriteria): Criteria => { + return criteria[1]; +}; + +export const hasGroupBy = (alertParams: AlertParams) => { + const { groupBy } = alertParams; + return groupBy && groupBy.length > 0 ? true : false; +}; + +// Chart previews // const chartPreviewHistogramBucket = rt.type({ key: rt.number, doc_count: rt.number, }); +// ES query responses // export const UngroupedSearchQueryResponseRT = rt.intersection([ commonSearchSuccessResponseFieldsRT, rt.intersection([ diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 15914bd1b220..3226287d4cbd 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -5,7 +5,12 @@ */ import * as rt from 'io-ts'; -import { criteriaRT, TimeUnitRT, timeSizeRT, groupByRT } from '../../alerting/logs/types'; +import { + criteriaRT, + TimeUnitRT, + timeSizeRT, + groupByRT, +} from '../../alerting/logs/log_threshold/types'; export const LOG_ALERTS_CHART_PREVIEW_DATA_PATH = '/api/infra/log_alerts/chart_preview_data'; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx deleted file mode 100644 index 627ea2bbef42..000000000000 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiFlexItem, EuiFlexGroup, EuiAccordion } from '@elastic/eui'; -import { IFieldType } from 'src/plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; -import { Criterion } from './criterion'; -import { - LogDocumentCountAlertParams, - Criterion as CriterionType, -} from '../../../../../common/alerting/logs/types'; -import { AlertsContext } from './editor'; -import { CriterionPreview } from './criterion_preview_chart'; - -interface Props { - fields: IFieldType[]; - criteria?: LogDocumentCountAlertParams['criteria']; - updateCriterion: (idx: number, params: Partial) => void; - removeCriterion: (idx: number) => void; - errors: IErrorObject; - alertParams: Partial; - context: AlertsContext; - sourceId: string; -} - -export const Criteria: React.FC = ({ - fields, - criteria, - updateCriterion, - removeCriterion, - errors, - alertParams, - context, - sourceId, -}) => { - if (!criteria) return null; - return ( - - - {criteria.map((criterion, idx) => { - return ( - 1} - errors={errors[idx.toString()] as IErrorObject} - /> - } - key={idx} - arrowDisplay="right" - > - - - ); - })} - - - ); -}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx deleted file mode 100644 index ff6a8e7e55fd..000000000000 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiPopoverTitle, - EuiFlexItem, - EuiFlexGroup, - EuiPopover, - EuiSelect, - EuiFieldNumber, - EuiExpression, - EuiFormRow, -} from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; -import { - Comparator, - ComparatorToi18nMap, - LogDocumentCountAlertParams, -} from '../../../../../common/alerting/logs/types'; - -const documentCountPrefix = i18n.translate('xpack.infra.logs.alertFlyout.documentCountPrefix', { - defaultMessage: 'when', -}); - -const getComparatorOptions = (): Array<{ - value: Comparator; - text: string; -}> => { - return [ - { value: Comparator.LT, text: ComparatorToi18nMap[Comparator.LT] }, - { value: Comparator.LT_OR_EQ, text: ComparatorToi18nMap[Comparator.LT_OR_EQ] }, - { value: Comparator.GT, text: ComparatorToi18nMap[Comparator.GT] }, - { value: Comparator.GT_OR_EQ, text: ComparatorToi18nMap[Comparator.GT_OR_EQ] }, - ]; -}; - -interface Props { - comparator?: Comparator; - value?: number; - updateCount: (params: Partial) => void; - errors: IErrorObject; -} - -export const DocumentCount: React.FC = ({ comparator, value, updateCount, errors }) => { - const [isComparatorPopoverOpen, setComparatorPopoverOpenState] = useState(false); - const [isValuePopoverOpen, setIsValuePopoverOpen] = useState(false); - - const documentCountValue = i18n.translate('xpack.infra.logs.alertFlyout.documentCountValue', { - defaultMessage: '{value, plural, one {log entry} other {log entries}}', - values: { value }, - }); - - const documentCountSuffix = i18n.translate('xpack.infra.logs.alertFlyout.documentCountSuffix', { - defaultMessage: '{value, plural, one {occurs} other {occur}}', - values: { value }, - }); - - return ( - - - setComparatorPopoverOpenState(true)} - /> - } - isOpen={isComparatorPopoverOpen} - closePopover={() => setComparatorPopoverOpenState(false)} - ownFocus - panelPaddingSize="s" - anchorPosition="downLeft" - > -
- {documentCountPrefix} - updateCount({ comparator: e.target.value as Comparator })} - options={getComparatorOptions()} - /> -
-
-
- - - setIsValuePopoverOpen(true)} - color={errors.value.length === 0 ? 'secondary' : 'danger'} - /> - } - isOpen={isValuePopoverOpen} - closePopover={() => setIsValuePopoverOpen(false)} - ownFocus - panelPaddingSize="s" - anchorPosition="downLeft" - > -
- {documentCountValue} - 0} error={errors.value}> - { - const number = parseInt(e.target.value, 10); - updateCount({ value: number ? number : undefined }); - }} - /> - -
-
-
- - - - -
- ); -}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/alert_dropdown.tsx similarity index 97% rename from x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx rename to x-pack/plugins/infra/public/components/alerting/logs/log_threshold/alert_dropdown.tsx index b8eb73b99f45..74634bbd5d29 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/alert_dropdown.tsx @@ -8,7 +8,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AlertFlyout } from './alert_flyout'; -import { useLinkProps } from '../../../hooks/use_link_props'; +import { useLinkProps } from '../../../../hooks/use_link_props'; export const AlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); diff --git a/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/alert_flyout.tsx similarity index 84% rename from x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx rename to x-pack/plugins/infra/public/components/alerting/logs/log_threshold/alert_flyout.tsx index 45e4f8576892..c6e16dcc9aae 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/alert_flyout.tsx @@ -6,11 +6,10 @@ import React, { useContext } from 'react'; import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public'; - -import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; -import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/types'; +import { AlertsContextProvider, AlertAdd } from '../../../../../../triggers_actions_ui/public'; +import { TriggerActionsContext } from '../../../../utils/triggers_actions_context'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../../common/alerting/logs/log_threshold/types'; interface Props { visible?: boolean; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criteria.tsx new file mode 100644 index 000000000000..a607b5ebf997 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criteria.tsx @@ -0,0 +1,284 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { EuiFlexItem, EuiFlexGroup, EuiButtonEmpty, EuiAccordion, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { IFieldType } from 'src/plugins/data/public'; +import { Criterion } from './criterion'; +import { + AlertParams, + Comparator, + Criteria as CriteriaType, + Criterion as CriterionType, + CountCriteria as CountCriteriaType, + RatioCriteria as RatioCriteriaType, + isRatioAlert, + getNumerator, + getDenominator, +} from '../../../../../../common/alerting/logs/log_threshold/types'; +import { AlertsContext, ExpressionLike } from './editor'; +import { CriterionPreview } from './criterion_preview_chart'; +import { Errors, CriterionErrors } from '../validation'; + +const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; + +const QueryAText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCriteriaQueryAText', { + defaultMessage: 'Query A', +}); + +const QueryBText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCriteriaQueryBText', { + defaultMessage: 'Query B', +}); + +interface SharedProps { + fields: IFieldType[]; + criteria?: AlertParams['criteria']; + errors: Errors['criteria']; + alertParams: Partial; + context: AlertsContext; + sourceId: string; + updateCriteria: (criteria: AlertParams['criteria']) => void; +} + +type CriteriaProps = SharedProps; + +export const Criteria: React.FC = (props) => { + const { criteria, errors } = props; + if (!criteria || criteria.length === 0) return null; + + return !isRatioAlert(criteria) ? ( + + ) : ( + + ); +}; + +interface CriteriaWrapperProps { + alertParams: SharedProps['alertParams']; + fields: SharedProps['fields']; + updateCriterion: (idx: number, params: Partial) => void; + removeCriterion: (idx: number) => void; + addCriterion: () => void; + criteria: CriteriaType; + errors: CriterionErrors; + context: SharedProps['context']; + sourceId: SharedProps['sourceId']; + isRatio?: boolean; +} + +const CriteriaWrapper: React.FC = (props) => { + const { + updateCriterion, + removeCriterion, + addCriterion, + criteria, + fields, + errors, + alertParams, + context, + sourceId, + isRatio = false, + } = props; + + return ( + + + {criteria.map((criterion, idx) => { + return ( + 1} + errors={errors[idx]} + /> + } + key={idx} + arrowDisplay="right" + > + + + ); + })} + + + + ); +}; + +interface RatioCriteriaProps { + alertParams: SharedProps['alertParams']; + fields: SharedProps['fields']; + criteria: RatioCriteriaType; + errors: Errors['criteria']; + context: SharedProps['context']; + sourceId: SharedProps['sourceId']; + updateCriteria: (criteria: AlertParams['criteria']) => void; +} + +const RatioCriteria: React.FC = (props) => { + const { criteria, errors, updateCriteria } = props; + + const handleUpdateNumeratorCriteria = useCallback( + (criteriaParam: CriteriaType) => { + const nextCriteria: RatioCriteriaType = [criteriaParam, getDenominator(criteria)]; + updateCriteria(nextCriteria); + }, + [updateCriteria, criteria] + ); + + const handleUpdateDenominatorCriteria = useCallback( + (criteriaParam: CriteriaType) => { + const nextCriteria: RatioCriteriaType = [getNumerator(criteria), criteriaParam]; + updateCriteria(nextCriteria); + }, + [updateCriteria, criteria] + ); + + const { + updateCriterion: updateNumeratorCriterion, + addCriterion: addNumeratorCriterion, + removeCriterion: removeNumeratorCriterion, + } = useCriteriaState(getNumerator(criteria), handleUpdateNumeratorCriteria); + + const { + updateCriterion: updateDenominatorCriterion, + addCriterion: addDenominatorCriterion, + removeCriterion: removeDenominatorCriterion, + } = useCriteriaState(getDenominator(criteria), handleUpdateDenominatorCriteria); + + return ( + <> + + + + + + + + + + + + + ); +}; + +interface CountCriteriaProps { + alertParams: SharedProps['alertParams']; + fields: SharedProps['fields']; + criteria: CountCriteriaType; + errors: Errors['criteria']; + context: SharedProps['context']; + sourceId: SharedProps['sourceId']; + updateCriteria: (criteria: AlertParams['criteria']) => void; +} + +const CountCriteria: React.FC = (props) => { + const { criteria, updateCriteria, errors } = props; + + const handleUpdateCriteria = useCallback( + (criteriaParam: CriteriaType) => { + updateCriteria(criteriaParam); + }, + [updateCriteria] + ); + + const { updateCriterion, addCriterion, removeCriterion } = useCriteriaState( + criteria, + handleUpdateCriteria + ); + + return ( + + ); +}; + +const useCriteriaState = ( + criteria: CriteriaType, + onUpdateCriteria: (criteria: CriteriaType) => void +) => { + const updateCriterion = useCallback( + (idx, criterionParams) => { + const nextCriteria = criteria.map((criterion, index) => { + return idx === index ? { ...criterion, ...criterionParams } : criterion; + }); + onUpdateCriteria(nextCriteria); + }, + [criteria, onUpdateCriteria] + ); + + const addCriterion = useCallback(() => { + const nextCriteria = criteria ? [...criteria, DEFAULT_CRITERIA] : [DEFAULT_CRITERIA]; + onUpdateCriteria(nextCriteria); + }, [criteria, onUpdateCriteria]); + + const removeCriterion = useCallback( + (idx) => { + const nextCriteria = criteria.filter((criterion, index) => { + return index !== idx; + }); + onUpdateCriteria(nextCriteria); + }, + [criteria, onUpdateCriteria] + ); + + return { updateCriterion, addCriterion, removeCriterion }; +}; + +const AddCriterionButton = ({ addCriterion }: { addCriterion: () => void }) => { + return ( +
+ + + +
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criterion.tsx similarity index 95% rename from x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx rename to x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criterion.tsx index 9ee9373bd2c1..8ecd172c08d2 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criterion.tsx @@ -18,14 +18,15 @@ import { EuiFormRow, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { isNumber, isFinite } from 'lodash'; import { IFieldType } from 'src/plugins/data/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { IErrorObject } from '../../../../../../../triggers_actions_ui/public/types'; import { Comparator, Criterion as CriterionType, ComparatorToi18nMap, -} from '../../../../../common/alerting/logs/types'; +} from '../../../../../../common/alerting/logs/log_threshold/types'; const firstCriterionFieldPrefix = i18n.translate( 'xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix', @@ -239,8 +240,10 @@ export const Criterion: React.FC = ({ compressed value={criterion.value as number | undefined} onChange={(e) => { - const number = parseInt(e.target.value, 10); - updateCriterion(idx, { value: number ? number : undefined }); + const number = parseFloat(e.target.value); + updateCriterion(idx, { + value: isNumber(number) && isFinite(number) ? number : undefined, + }); }} /> ) : ( diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criterion_preview_chart.tsx similarity index 87% rename from x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion_preview_chart.tsx rename to x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criterion_preview_chart.tsx index 31f9a64015c0..675900499e79 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/criterion_preview_chart.tsx @@ -31,28 +31,30 @@ import { getChartTheme, yAxisFormatter, NUM_BUCKETS, -} from '../../shared/criterion_preview_chart/criterion_preview_chart'; +} from '../../../shared/criterion_preview_chart/criterion_preview_chart'; import { - LogDocumentCountAlertParams, + AlertParams, + Threshold, Criterion, Comparator, -} from '../../../../../common/alerting/logs/types'; -import { Color, colorTransformer } from '../../../../../common/color_palette'; +} from '../../../../../../common/alerting/logs/log_threshold/types'; +import { Color, colorTransformer } from '../../../../../../common/color_palette'; import { GetLogAlertsChartPreviewDataAlertParamsSubset, getLogAlertsChartPreviewDataAlertParamsSubsetRT, -} from '../../../../../common/http_api/log_alerts/'; +} from '../../../../../../common/http_api/log_alerts/'; import { AlertsContext } from './editor'; import { useChartPreviewData } from './hooks/use_chart_preview_data'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { decodeOrThrow } from '../../../../../../common/runtime_types'; const GROUP_LIMIT = 5; interface Props { - alertParams: Partial; + alertParams: Partial; context: AlertsContext; chartCriterion: Partial; sourceId: string; + showThreshold: boolean; } export const CriterionPreview: React.FC = ({ @@ -60,6 +62,7 @@ export const CriterionPreview: React.FC = ({ context, chartCriterion, sourceId, + showThreshold, }) => { const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => { const { field, comparator, value } = chartCriterion; @@ -92,6 +95,7 @@ export const CriterionPreview: React.FC = ({ sourceId={sourceId} threshold={alertParams.count} chartAlertParams={chartAlertParams} + showThreshold={showThreshold} /> ); }; @@ -100,8 +104,9 @@ interface ChartProps { buckets: number; context: AlertsContext; sourceId: string; - threshold?: LogDocumentCountAlertParams['count']; + threshold?: Threshold; chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset; + showThreshold: boolean; } const CriterionPreviewChart: React.FC = ({ @@ -110,6 +115,7 @@ const CriterionPreviewChart: React.FC = ({ sourceId, threshold, chartAlertParams, + showThreshold, }) => { const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; @@ -140,17 +146,18 @@ const CriterionPreviewChart: React.FC = ({ const isGrouped = groupBy && groupBy.length > 0 ? true : false; const isAbove = - threshold && threshold.comparator + showThreshold && threshold && threshold.comparator ? [Comparator.GT, Comparator.GT_OR_EQ].includes(threshold.comparator) : false; const isBelow = - threshold && threshold.comparator + showThreshold && threshold && threshold.comparator ? [Comparator.LT, Comparator.LT_OR_EQ].includes(threshold.comparator) : false; // For grouped scenarios we want to limit the groups displayed, for "isAbove" thresholds we'll show // groups with the highest doc counts. And for "isBelow" thresholds we'll show groups with the lowest doc counts. + // Ratio scenarios will just default to max. const filteredSeries = useMemo(() => { if (!isGrouped) { return series; @@ -183,11 +190,14 @@ const CriterionPreviewChart: React.FC = ({ const hasData = series.length > 0; const { yMin, yMax, xMin, xMax } = getDomain(filteredSeries, isStacked); const chartDomain = { - max: threshold && threshold.value ? Math.max(yMax, threshold.value) * 1.1 : yMax * 1.1, // Add 10% headroom. - min: threshold && threshold.value ? Math.min(yMin, threshold.value) : yMin, + max: + showThreshold && threshold && threshold.value + ? Math.max(yMax, threshold.value) * 1.1 + : yMax * 1.1, // Add 10% headroom. + min: showThreshold && threshold && threshold.value ? Math.min(yMin, threshold.value) : yMin, }; - if (threshold && threshold.value && chartDomain.min === threshold.value) { + if (showThreshold && threshold && threshold.value && chartDomain.min === threshold.value) { chartDomain.min = chartDomain.min * 0.9; // Allow some padding so the threshold annotation has better visibility } @@ -229,7 +239,7 @@ const CriterionPreviewChart: React.FC = ({ }} color={!isGrouped ? colorTransformer(Color.color0) : undefined} /> - {threshold && threshold.value ? ( + {showThreshold && threshold && threshold.value ? ( = ({ }} /> ) : null} - {threshold && threshold.value && isBelow ? ( + {showThreshold && threshold && threshold.value && isBelow ? ( = ({ ]} /> ) : null} - {threshold && threshold.value && isAbove ? ( + {showThreshold && threshold && threshold.value && isAbove ? ( ; interface Props { - errors: IErrorObject; - alertParams: Partial; + errors: Errors; + alertParams: Partial; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; alertsContext: AlertsContext; @@ -46,14 +50,30 @@ interface Props { const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; -const DEFAULT_EXPRESSION = { +const DEFAULT_BASE_EXPRESSION = { + timeSize: 5, + timeUnit: 'm', +}; + +const DEFAULT_COUNT_EXPRESSION = { + ...DEFAULT_BASE_EXPRESSION, count: { value: 75, comparator: Comparator.GT, }, criteria: [DEFAULT_CRITERIA], - timeSize: 5, - timeUnit: 'm', +}; + +const DEFAULT_RATIO_EXPRESSION = { + ...DEFAULT_BASE_EXPRESSION, + count: { + value: 2, + comparator: Comparator.GT, + }, + criteria: [ + [DEFAULT_CRITERIA], + [{ field: 'log.level', comparator: Comparator.EQ, value: 'warning' }], + ], }; export const ExpressionEditor: React.FC = (props) => { @@ -125,10 +145,10 @@ export const Editor: React.FC = (props) => { const [hasSetDefaults, setHasSetDefaults] = useState(false); const { sourceStatus } = useLogSourceContext(); useMount(() => { - for (const [key, value] of Object.entries({ ...DEFAULT_EXPRESSION, ...alertParams })) { + for (const [key, value] of Object.entries({ ...DEFAULT_COUNT_EXPRESSION, ...alertParams })) { setAlertParams(key, value); - setHasSetDefaults(true); } + setHasSetDefaults(true); }); const supportedFields = useMemo(() => { @@ -153,22 +173,19 @@ export const Editor: React.FC = (props) => { /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [sourceStatus]); - const updateCount = useCallback( - (countParams) => { - const nextCountParams = { ...alertParams.count, ...countParams }; - setAlertParams('count', nextCountParams); + const updateThreshold = useCallback( + (thresholdParams) => { + const nextThresholdParams = { ...alertParams.count, ...thresholdParams }; + setAlertParams('count', nextThresholdParams); }, [alertParams.count, setAlertParams] ); - const updateCriterion = useCallback( - (idx, criterionParams) => { - const nextCriteria = alertParams.criteria?.map((criterion, index) => { - return idx === index ? { ...criterion, ...criterionParams } : criterion; - }); - setAlertParams('criteria', nextCriteria ? nextCriteria : []); + const updateCriteria = useCallback( + (criteria: AlertParams['criteria']) => { + setAlertParams('criteria', criteria); }, - [alertParams, setAlertParams] + [setAlertParams] ); const updateTimeSize = useCallback( @@ -192,46 +209,46 @@ export const Editor: React.FC = (props) => { [setAlertParams] ); - const addCriterion = useCallback(() => { - const nextCriteria = alertParams?.criteria - ? [...alertParams.criteria, DEFAULT_CRITERIA] - : [DEFAULT_CRITERIA]; - setAlertParams('criteria', nextCriteria); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [alertParams, setAlertParams]); - - const removeCriterion = useCallback( - (idx) => { - const nextCriteria = alertParams?.criteria?.filter((criterion, index) => { - return index !== idx; - }); - setAlertParams('criteria', nextCriteria); + const updateType = useCallback( + (type: ThresholdType) => { + const defaults = type === 'count' ? DEFAULT_COUNT_EXPRESSION : DEFAULT_RATIO_EXPRESSION; + // Reset properties that don't make sense switching from one context to the other + for (const [key, value] of Object.entries({ + criteria: defaults.criteria, + count: defaults.count, + })) { + setAlertParams(key, value); + } }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - [alertParams, setAlertParams] + [setAlertParams] ); // Wait until the alert param defaults have been set if (!hasSetDefaults) return null; + const criteriaComponent = alertParams.criteria ? ( + + ) : null; + return ( <> - + + {alertParams.criteria && !isRatioAlert(alertParams.criteria) && criteriaComponent} + + - - = (props) => { timeWindowUnit={alertParams.timeUnit} onChangeWindowSize={updateTimeSize} onChangeWindowUnit={updateTimeUnit} - errors={errors as { [key: string]: string[] }} + errors={{ timeWindowSize: errors.timeWindowSize, timeSizeUnit: errors.timeSizeUnit }} /> = (props) => { fields={groupByFields} /> -
- - - -
+ {alertParams.criteria && isRatioAlert(alertParams.criteria) && criteriaComponent} + + ); }; @@ -269,3 +275,13 @@ export const Editor: React.FC = (props) => { // required for dynamic import // eslint-disable-next-line import/no-default-export export default ExpressionEditor; + +// NOTE: Temporary until EUI allow empty values in EuiExpression +// components. +export const ExpressionLike = ({ text }: { text: string }) => { + return ( +
+ {text} +
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/hooks/use_chart_preview_data.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/hooks/use_chart_preview_data.tsx similarity index 90% rename from x-pack/plugins/infra/public/components/alerting/logs/expression_editor/hooks/use_chart_preview_data.tsx rename to x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/hooks/use_chart_preview_data.tsx index d5ba730026b1..d43e291f900f 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/hooks/use_chart_preview_data.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/hooks/use_chart_preview_data.tsx @@ -5,15 +5,15 @@ */ import { useState, useMemo } from 'react'; import { AlertsContext } from '../editor'; -import { useTrackedPromise } from '../../../../../utils/use_tracked_promise'; +import { useTrackedPromise } from '../../../../../../utils/use_tracked_promise'; import { GetLogAlertsChartPreviewDataSuccessResponsePayload, getLogAlertsChartPreviewDataSuccessResponsePayloadRT, getLogAlertsChartPreviewDataRequestPayloadRT, LOG_ALERTS_CHART_PREVIEW_DATA_PATH, -} from '../../../../../../common/http_api'; -import { decodeOrThrow } from '../../../../../../common/runtime_types'; -import { GetLogAlertsChartPreviewDataAlertParamsSubset } from '../../../../../../common/http_api/log_alerts/'; +} from '../../../../../../../common/http_api'; +import { decodeOrThrow } from '../../../../../../../common/runtime_types'; +import { GetLogAlertsChartPreviewDataAlertParamsSubset } from '../../../../../../../common/http_api/log_alerts/'; interface Options { sourceId: string; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/index.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/index.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/alerting/logs/expression_editor/index.tsx rename to x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/index.tsx diff --git a/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/threshold.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/threshold.tsx new file mode 100644 index 000000000000..e2065ca25cb6 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/threshold.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isNumber, isFinite } from 'lodash'; +import { + EuiPopoverTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiSelect, + EuiFieldNumber, + EuiExpression, + EuiFormRow, +} from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../../triggers_actions_ui/public/types'; +import { + Comparator, + ComparatorToi18nMap, + AlertParams, +} from '../../../../../../common/alerting/logs/log_threshold/types'; + +const thresholdPrefix = i18n.translate('xpack.infra.logs.alertFlyout.thresholdPrefix', { + defaultMessage: 'is', +}); + +const popoverTitle = i18n.translate('xpack.infra.logs.alertFlyout.thresholdPopoverTitle', { + defaultMessage: 'Threshold', +}); + +const getComparatorOptions = (): Array<{ + value: Comparator; + text: string; +}> => { + return [ + { value: Comparator.LT, text: ComparatorToi18nMap[Comparator.LT] }, + { value: Comparator.LT_OR_EQ, text: ComparatorToi18nMap[Comparator.LT_OR_EQ] }, + { value: Comparator.GT, text: ComparatorToi18nMap[Comparator.GT] }, + { value: Comparator.GT_OR_EQ, text: ComparatorToi18nMap[Comparator.GT_OR_EQ] }, + ]; +}; + +interface Props { + comparator?: Comparator; + value?: number; + updateThreshold: (params: Partial) => void; + errors: IErrorObject; +} + +export const Threshold: React.FC = ({ comparator, value, updateThreshold, errors }) => { + const [isThresholdPopoverOpen, setThresholdPopoverOpenState] = useState(false); + + return ( + + + setThresholdPopoverOpenState(true)} + /> + } + isOpen={isThresholdPopoverOpen} + closePopover={() => setThresholdPopoverOpenState(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > + <> + {popoverTitle} + + + + updateThreshold({ comparator: e.target.value as Comparator })} + options={getComparatorOptions()} + /> + + + + 0} error={errors.value}> + { + const number = parseFloat(e.target.value); + updateThreshold({ + value: isNumber(number) && isFinite(number) ? number : undefined, + }); + }} + /> + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/type_switcher.tsx b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/type_switcher.tsx new file mode 100644 index 000000000000..03c895dcd0f2 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/expression_editor/type_switcher.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexItem, EuiFlexGroup, EuiPopover, EuiSelect, EuiExpression } from '@elastic/eui'; +import { + AlertParams, + ThresholdType, + isRatioAlert, +} from '../../../../../../common/alerting/logs/log_threshold/types'; +import { ExpressionLike } from './editor'; + +const typePrefix = i18n.translate('xpack.infra.logs.alertFlyout.thresholdTypePrefix', { + defaultMessage: 'when the', +}); + +const countSuffix = i18n.translate('xpack.infra.logs.alertFlyout.thresholdTypeCountSuffix', { + defaultMessage: 'of log entries', +}); + +const ratioSuffix = i18n.translate('xpack.infra.logs.alertFlyout.thresholdTypeRatioSuffix', { + defaultMessage: 'of Query A to Query B', +}); + +const countI18n = i18n.translate('xpack.infra.logs.alertFlyout.thresholdTypeCount', { + defaultMessage: 'count', +}); + +const ratioI18n = i18n.translate('xpack.infra.logs.alertFlyout.thresholdTypeRatio', { + defaultMessage: 'ratio', +}); + +const getOptions = (): Array<{ + value: ThresholdType; + text: string; +}> => { + return [ + { value: 'ratio', text: ratioI18n }, + { value: 'count', text: countI18n }, + ]; +}; + +interface Props { + criteria: AlertParams['criteria']; + updateType: (type: ThresholdType) => void; +} + +const getThresholdType = (criteria: AlertParams['criteria']): ThresholdType => { + return isRatioAlert(criteria) ? 'ratio' : 'count'; +}; + +export const TypeSwitcher: React.FC = ({ criteria, updateType }) => { + const [isThresholdTypePopoverOpen, setThresholdTypePopoverOpenState] = useState(false); + const thresholdType = getThresholdType(criteria); + + return ( + + + + setThresholdTypePopoverOpenState(true)} + /> + + + } + isOpen={isThresholdTypePopoverOpen} + closePopover={() => setThresholdTypePopoverOpenState(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > + + + updateType(thresholdType === 'ratio' ? 'count' : 'ratio')} + options={getOptions()} + /> + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/log_threshold_alert_type.ts similarity index 53% rename from x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts rename to x-pack/plugins/infra/public/components/alerting/logs/log_threshold/log_threshold_alert_type.ts index a26a7328c910..15ff5844c123 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/log_threshold_alert_type.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types'; -import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/types'; +import { AlertTypeModel } from '../../../../../../triggers_actions_ui/public/types'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../../common/alerting/logs/log_threshold/types'; import { validateExpression } from './validation'; export function getAlertType(): AlertTypeModel { @@ -22,7 +22,7 @@ export function getAlertType(): AlertTypeModel { defaultActionMessage: i18n.translate( 'xpack.infra.logs.alerting.threshold.defaultActionMessage', { - defaultMessage: `\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\}\\{\\{context.matchingDocuments\\}\\} log entries have matched the following conditions: \\{\\{context.conditions\\}\\}`, + defaultMessage: `\\{\\{^context.isRatio\\}\\}\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\}\\{\\{context.matchingDocuments\\}\\} log entries have matched the following conditions: \\{\\{context.conditions\\}\\}\\{\\{/context.isRatio\\}\\}\\{\\{#context.isRatio\\}\\}\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\} Ratio of the count of log entries matching \\{\\{context.numeratorConditions\\}\\} to the count of log entries matching \\{\\{context.denominatorConditions\\}\\} was \\{\\{context.ratio\\}\\}\\{\\{/context.isRatio\\}\\}`, } ), requiresAppContext: false, diff --git a/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/validation.ts b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/validation.ts new file mode 100644 index 000000000000..a7f773c08d2b --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold/validation.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { isNumber, isFinite } from 'lodash'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../../triggers_actions_ui/public/types'; +import { + AlertParams, + Criteria, + RatioCriteria, + isRatioAlert, + getNumerator, + getDenominator, +} from '../../../../../common/alerting/logs/log_threshold/types'; + +export interface CriterionErrors { + [id: string]: { + field: string[]; + comparator: string[]; + value: string[]; + }; +} + +export interface Errors { + threshold: { + value: string[]; + }; + // NOTE: The data structure for criteria errors isn't 100% + // ideal but we need to conform to the interfaces that the alerting + // framework expects. + criteria: { + [id: string]: CriterionErrors; + }; + timeWindowSize: string[]; + timeSizeUnit: string[]; +} + +export function validateExpression({ + count, + criteria, + timeSize, + timeUnit, +}: Partial): ValidationResult { + const validationResult = { errors: {} }; + + // NOTE: In the case of components provided by the Alerting framework the error property names + // must match what they expect. + const errors: Errors = { + threshold: { + value: [], + }, + criteria: {}, + timeSizeUnit: [], + timeWindowSize: [], + }; + + validationResult.errors = errors; + + // Threshold validation + if (!isNumber(count?.value) && !isFinite(count?.value)) { + errors.threshold.value.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Numeric threshold value is Required.', + }) + ); + } + + // Time validation + if (!timeSize) { + errors.timeWindowSize.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.timeSizeRequired', { + defaultMessage: 'Time size is Required.', + }) + ); + } + + // Criteria validation + if (criteria && criteria.length > 0) { + const getCriterionErrors = (_criteria: Criteria): CriterionErrors => { + const _errors: CriterionErrors = {}; + + _criteria.forEach((criterion, idx) => { + _errors[idx] = { + field: [], + comparator: [], + value: [], + }; + if (!criterion.field) { + _errors[idx].field.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionFieldRequired', { + defaultMessage: 'Field is required.', + }) + ); + } + if (!criterion.comparator) { + _errors[idx].comparator.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionComparatorRequired', { + defaultMessage: 'Comparator is required.', + }) + ); + } + if (criterion.value === undefined || criterion.value === null) { + _errors[idx].value.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionValueRequired', { + defaultMessage: 'Value is required.', + }) + ); + } + }); + return _errors; + }; + + if (!isRatioAlert(criteria)) { + const criteriaErrors = getCriterionErrors(criteria as Criteria); + errors.criteria[0] = criteriaErrors; + } else { + const numeratorErrors = getCriterionErrors(getNumerator(criteria as RatioCriteria)); + errors.criteria[0] = numeratorErrors; + const denominatorErrors = getCriterionErrors(getDenominator(criteria as RatioCriteria)); + errors.criteria[1] = denominatorErrors; + } + } + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/components/alerting/logs/validation.ts b/x-pack/plugins/infra/public/components/alerting/logs/validation.ts deleted file mode 100644 index c8c513f57a9d..000000000000 --- a/x-pack/plugins/infra/public/components/alerting/logs/validation.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; -import { LogDocumentCountAlertParams } from '../../../../common/alerting/logs/types'; - -export function validateExpression({ - count, - criteria, - timeSize, - timeUnit, -}: Partial): ValidationResult { - const validationResult = { errors: {} }; - - // NOTE: In the case of components provided by the Alerting framework the error property names - // must match what they expect. - const errors: { - count: { - value: string[]; - }; - criteria: { - [id: string]: { - field: string[]; - comparator: string[]; - value: string[]; - }; - }; - timeWindowSize: string[]; - timeSizeUnit: string[]; - } = { - count: { - value: [], - }, - criteria: {}, - timeSizeUnit: [], - timeWindowSize: [], - }; - - validationResult.errors = errors; - - // Document count validation - if (typeof count?.value !== 'number') { - errors.count.value.push( - i18n.translate('xpack.infra.logs.alertFlyout.error.documentCountRequired', { - defaultMessage: 'Document count is Required.', - }) - ); - } - - // Time validation - if (!timeSize) { - errors.timeWindowSize.push( - i18n.translate('xpack.infra.logs.alertFlyout.error.timeSizeRequired', { - defaultMessage: 'Time size is Required.', - }) - ); - } - - if (criteria && criteria.length > 0) { - // Criteria validation - criteria.forEach((criterion, idx: number) => { - const id = idx.toString(); - - errors.criteria[id] = { - field: [], - comparator: [], - value: [], - }; - - if (!criterion.field) { - errors.criteria[id].field.push( - i18n.translate('xpack.infra.logs.alertFlyout.error.criterionFieldRequired', { - defaultMessage: 'Field is required.', - }) - ); - } - - if (!criterion.comparator) { - errors.criteria[id].comparator.push( - i18n.translate('xpack.infra.logs.alertFlyout.error.criterionComparatorRequired', { - defaultMessage: 'Comparator is required.', - }) - ); - } - - if (!criterion.value) { - errors.criteria[id].value.push( - i18n.translate('xpack.infra.logs.alertFlyout.error.criterionValueRequired', { - defaultMessage: 'Value is required.', - }) - ); - } - }); - } - - return validationResult; -} diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index 426ae8e9d05a..973037af499e 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -23,7 +23,7 @@ import { LogEntryCategoriesPage } from './log_entry_categories'; import { LogEntryRatePage } from './log_entry_rate'; import { LogsSettingsPage } from './settings'; import { StreamPage } from './stream'; -import { AlertDropdown } from '../../components/alerting/logs/alert_dropdown'; +import { AlertDropdown } from '../../components/alerting/logs/log_threshold/alert_dropdown'; export const LogsPageContent: React.FunctionComponent = () => { const uiCapabilities = useKibana().services.application?.capabilities; diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 66715b3fee28..b409c32603ff 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -8,7 +8,7 @@ import { AppMountParameters, PluginInitializerContext } from 'kibana/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { createMetricThresholdAlertType } from './alerting/metric_threshold'; import { createInventoryMetricAlertType } from './alerting/inventory'; -import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; +import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold/log_threshold_alert_type'; import { registerStartSingleton } from './legacy_singletons'; import { registerFeatures } from './register_feature'; import { diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 12ac57eb9018..444530c4d79f 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/types'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../common/alerting/logs/log_threshold/types'; import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/inventory_metric_threshold/types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts index 71115ad3a574..e1657968b3f9 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts @@ -23,7 +23,7 @@ import { UngroupedSearchQueryResponse, GroupedSearchQueryResponse, GroupedSearchQueryResponseRT, -} from '../../../../common/alerting/logs/types'; +} from '../../../../common/alerting/logs/log_threshold/types'; import { decodeOrThrow } from '../../../../common/runtime_types'; const COMPOSITE_GROUP_SIZE = 40; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index f730513991a7..e04fe338f343 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -17,11 +17,11 @@ import { import { Comparator, AlertStates, - LogDocumentCountAlertParams, + AlertParams, Criterion, UngroupedSearchQueryResponse, GroupedSearchQueryResponse, -} from '../../../../common/alerting/logs/types'; +} from '../../../../common/alerting/logs/log_threshold/types'; import { alertsMock } from '../../../../../alerts/server/mocks'; // Mocks // @@ -56,7 +56,7 @@ const negativeCriteria: Criterion[] = [ { ...textField, comparator: Comparator.NOT_MATCH_PHRASE }, ]; -const baseAlertParams: Pick = { +const baseAlertParams: Pick = { count: { comparator: Comparator.GT, value: 5, @@ -85,7 +85,7 @@ describe('Log threshold executor', () => { }); describe('Criteria filter building', () => { test('Handles positive criteria', () => { - const alertParams: LogDocumentCountAlertParams = { + const alertParams: AlertParams = { ...baseAlertParams, criteria: positiveCriteria, }; @@ -140,7 +140,7 @@ describe('Log threshold executor', () => { }); test('Handles negative criteria', () => { - const alertParams: LogDocumentCountAlertParams = { + const alertParams: AlertParams = { ...baseAlertParams, criteria: negativeCriteria, }; @@ -168,7 +168,7 @@ describe('Log threshold executor', () => { }); test('Handles time range', () => { - const alertParams: LogDocumentCountAlertParams = { ...baseAlertParams, criteria: [] }; + const alertParams: AlertParams = { ...baseAlertParams, criteria: [] }; const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number'); expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number'); @@ -183,7 +183,7 @@ describe('Log threshold executor', () => { describe('ES queries', () => { describe('Query generation', () => { test('Correctly generates ungrouped queries', () => { - const alertParams: LogDocumentCountAlertParams = { + const alertParams: AlertParams = { ...baseAlertParams, criteria: [...positiveCriteria, ...negativeCriteria], }; @@ -279,7 +279,7 @@ describe('Log threshold executor', () => { }); test('Correctly generates grouped queries', () => { - const alertParams: LogDocumentCountAlertParams = { + const alertParams: AlertParams = { ...baseAlertParams, groupBy: ['host.name'], criteria: [...positiveCriteria, ...negativeCriteria], @@ -303,25 +303,6 @@ describe('Log threshold executor', () => { }, }, ], - must_not: [ - { - term: { - keywordField: { - value: 'error', - }, - }, - }, - { - match: { - textField: 'Something went wrong', - }, - }, - { - match_phrase: { - textField: 'Something went wrong', - }, - }, - ], }, }, aggregations: { @@ -398,6 +379,25 @@ describe('Log threshold executor', () => { }, }, ], + must_not: [ + { + term: { + keywordField: { + value: 'error', + }, + }, + }, + { + match: { + textField: 'Something went wrong', + }, + }, + { + match_phrase: { + textField: 'Something went wrong', + }, + }, + ], }, }, }, @@ -467,6 +467,7 @@ describe('Log threshold executor', () => { conditions: ' numericField more than 10', group: null, matchingDocuments: 10, + isRatio: false, }, }, ]); @@ -593,6 +594,7 @@ describe('Log threshold executor', () => { conditions: ' numericField more than 10', group: 'i-am-a-host-name-1, i-am-a-dataset-1', matchingDocuments: 10, + isRatio: false, }, }, ]); @@ -612,6 +614,7 @@ describe('Log threshold executor', () => { conditions: ' numericField more than 10', group: 'i-am-a-host-name-3, i-am-a-dataset-3', matchingDocuments: 20, + isRatio: false, }, }, ]); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 224b898141c3..0ea65f94c940 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -14,14 +14,21 @@ import { import { AlertStates, Comparator, - LogDocumentCountAlertParams, + AlertParams, Criterion, GroupedSearchQueryResponseRT, UngroupedSearchQueryResponseRT, UngroupedSearchQueryResponse, GroupedSearchQueryResponse, - LogDocumentCountAlertParamsRT, -} from '../../../../common/alerting/logs/types'; + AlertParamsRT, + isRatioAlertParams, + hasGroupBy, + getNumerator, + getDenominator, + Criteria, + CountAlertParams, + RatioAlertParams, +} from '../../../../common/alerting/logs/log_threshold/types'; import { InfraBackendLibs } from '../../infra_types'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { decodeOrThrow } from '../../../../common/runtime_types'; @@ -42,7 +49,6 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => async function ({ services, params }: AlertExecutorOptions) { const { alertInstanceFactory, savedObjectsClient, callCluster } = services; const { sources } = libs; - const { groupBy } = params; const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); const indexPattern = sourceConfiguration.configuration.logAlias; @@ -50,30 +56,23 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); try { - const validatedParams = decodeOrThrow(LogDocumentCountAlertParamsRT)(params); + const validatedParams = decodeOrThrow(AlertParamsRT)(params); - const query = - groupBy && groupBy.length > 0 - ? getGroupedESQuery(validatedParams, timestampField, indexPattern) - : getUngroupedESQuery(validatedParams, timestampField, indexPattern); - - if (!query) { - throw new Error('ES query could not be built from the provided alert params'); - } - - if (groupBy && groupBy.length > 0) { - processGroupByResults( - await getGroupedResults(query, callCluster), + if (!isRatioAlertParams(validatedParams)) { + await executeAlert( validatedParams, - alertInstanceFactory, - updateAlertInstance + timestampField, + indexPattern, + callCluster, + alertInstanceFactory ); } else { - processUngroupedResults( - await getUngroupedResults(query, callCluster), + await executeRatioAlert( validatedParams, - alertInstanceFactory, - updateAlertInstance + timestampField, + indexPattern, + callCluster, + alertInstanceFactory ); } } catch (e) { @@ -85,9 +84,97 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => } }; +async function executeAlert( + alertParams: CountAlertParams, + timestampField: string, + indexPattern: string, + callCluster: AlertServices['callCluster'], + alertInstanceFactory: AlertServices['alertInstanceFactory'] +) { + const query = getESQuery(alertParams, timestampField, indexPattern); + + if (!query) { + throw new Error('ES query could not be built from the provided alert params'); + } + + if (hasGroupBy(alertParams)) { + processGroupByResults( + await getGroupedResults(query, callCluster), + alertParams, + alertInstanceFactory, + updateAlertInstance + ); + } else { + processUngroupedResults( + await getUngroupedResults(query, callCluster), + alertParams, + alertInstanceFactory, + updateAlertInstance + ); + } +} + +async function executeRatioAlert( + alertParams: RatioAlertParams, + timestampField: string, + indexPattern: string, + callCluster: AlertServices['callCluster'], + alertInstanceFactory: AlertServices['alertInstanceFactory'] +) { + // Ratio alert params are separated out into two standard sets of alert params + const numeratorParams: AlertParams = { + ...alertParams, + criteria: getNumerator(alertParams.criteria), + }; + + const denominatorParams: AlertParams = { + ...alertParams, + criteria: getDenominator(alertParams.criteria), + }; + + const numeratorQuery = getESQuery(numeratorParams, timestampField, indexPattern); + const denominatorQuery = getESQuery(denominatorParams, timestampField, indexPattern); + + if (!numeratorQuery || !denominatorQuery) { + throw new Error('ES query could not be built from the provided ratio alert params'); + } + + if (hasGroupBy(alertParams)) { + const numeratorGroupedResults = await getGroupedResults(numeratorQuery, callCluster); + const denominatorGroupedResults = await getGroupedResults(denominatorQuery, callCluster); + processGroupByRatioResults( + numeratorGroupedResults, + denominatorGroupedResults, + alertParams, + alertInstanceFactory, + updateAlertInstance + ); + } else { + const numeratorUngroupedResults = await getUngroupedResults(numeratorQuery, callCluster); + const denominatorUngroupedResults = await getUngroupedResults(denominatorQuery, callCluster); + processUngroupedRatioResults( + numeratorUngroupedResults, + denominatorUngroupedResults, + alertParams, + alertInstanceFactory, + updateAlertInstance + ); + } +} + +const getESQuery = ( + alertParams: Omit & { criteria: Criteria }, + timestampField: string, + indexPattern: string +) => { + return hasGroupBy(alertParams) + ? getGroupedESQuery(alertParams, timestampField, indexPattern) + : getUngroupedESQuery(alertParams, timestampField, indexPattern); +}; + export const processUngroupedResults = ( results: UngroupedSearchQueryResponse, - params: LogDocumentCountAlertParams, + params: CountAlertParams, alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], alertInstaceUpdater: AlertInstanceUpdater ) => { @@ -102,8 +189,9 @@ export const processUngroupedResults = ( actionGroup: FIRED_ACTIONS.id, context: { matchingDocuments: documentCount, - conditions: createConditionsMessage(criteria), + conditions: createConditionsMessageForCriteria(criteria), group: null, + isRatio: false, }, }, ]); @@ -112,24 +200,71 @@ export const processUngroupedResults = ( } }; -interface ReducedGroupByResults { - name: string; - documentCount: number; -} - -export const processGroupByResults = ( - results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], - params: LogDocumentCountAlertParams, +export const processUngroupedRatioResults = ( + numeratorResults: UngroupedSearchQueryResponse, + denominatorResults: UngroupedSearchQueryResponse, + params: RatioAlertParams, alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; - const groupResults = results.reduce((acc, groupBucket) => { + const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); + const numeratorCount = numeratorResults.hits.total.value; + const denominatorCount = denominatorResults.hits.total.value; + const ratio = getRatio(numeratorCount, denominatorCount); + + if (ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value)) { + alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ + { + actionGroup: FIRED_ACTIONS.id, + context: { + ratio, + numeratorConditions: createConditionsMessageForCriteria(getNumerator(criteria)), + denominatorConditions: createConditionsMessageForCriteria(getDenominator(criteria)), + group: null, + isRatio: true, + }, + }, + ]); + } else { + alertInstaceUpdater(alertInstance, AlertStates.OK); + } +}; + +const getRatio = (numerator: number, denominator: number) => { + // We follow the mathematics principle that dividing by 0 isn't possible, + // and a ratio is therefore undefined (or indeterminate). + if (numerator === 0 || denominator === 0) return undefined; + return numerator / denominator; +}; + +interface ReducedGroupByResult { + name: string; + documentCount: number; +} + +type ReducedGroupByResults = ReducedGroupByResult[]; + +const getReducedGroupByResults = ( + results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] +): ReducedGroupByResults => { + return results.reduce((acc, groupBucket) => { const groupName = Object.values(groupBucket.key).join(', '); const groupResult = { name: groupName, documentCount: groupBucket.filtered_results.doc_count }; return [...acc, groupResult]; }, []); +}; + +export const processGroupByResults = ( + results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], + params: CountAlertParams, + alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertInstaceUpdater: AlertInstanceUpdater +) => { + const { count, criteria } = params; + + const groupResults = getReducedGroupByResults(results); groupResults.forEach((group) => { const alertInstance = alertInstanceFactory(group.name); @@ -141,8 +276,53 @@ export const processGroupByResults = ( actionGroup: FIRED_ACTIONS.id, context: { matchingDocuments: documentCount, - conditions: createConditionsMessage(criteria), + conditions: createConditionsMessageForCriteria(criteria), group: group.name, + isRatio: false, + }, + }, + ]); + } else { + alertInstaceUpdater(alertInstance, AlertStates.OK); + } + }); +}; + +export const processGroupByRatioResults = ( + numeratorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], + denominatorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], + params: RatioAlertParams, + alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertInstaceUpdater: AlertInstanceUpdater +) => { + const { count, criteria } = params; + + const numeratorGroupResults = getReducedGroupByResults(numeratorResults); + const denominatorGroupResults = getReducedGroupByResults(denominatorResults); + + numeratorGroupResults.forEach((numeratorGroup) => { + const alertInstance = alertInstanceFactory(numeratorGroup.name); + const numeratorDocumentCount = numeratorGroup.documentCount; + const denominatorGroup = denominatorGroupResults.find( + (_group) => _group.name === numeratorGroup.name + ); + // If there is no matching group, a ratio cannot be determined, and is therefore undefined. + const ratio = denominatorGroup + ? getRatio(numeratorDocumentCount, denominatorGroup.documentCount) + : undefined; + if ( + ratio !== undefined && + checkValueAgainstComparatorMap[count.comparator](ratio, count.value) + ) { + alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ + { + actionGroup: FIRED_ACTIONS.id, + context: { + ratio, + numeratorConditions: createConditionsMessageForCriteria(getNumerator(criteria)), + denominatorConditions: createConditionsMessageForCriteria(getDenominator(criteria)), + group: numeratorGroup.name, + isRatio: true, }, }, ]); @@ -172,7 +352,7 @@ export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state, }; export const buildFiltersFromCriteria = ( - params: Omit, + params: Pick & { criteria: Criteria }, timestampField: string ) => { const { timeSize, timeUnit, criteria } = params; @@ -223,7 +403,7 @@ export const buildFiltersFromCriteria = ( }; export const getGroupedESQuery = ( - params: Omit, + params: Pick & { criteria: Criteria }, timestampField: string, index: string ): object | undefined => { @@ -254,6 +434,7 @@ export const getGroupedESQuery = ( bool: { // Scope the inner filtering back to the unpadded range filter: [rangeFilter, ...mustFilters], + ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), }, }, }, @@ -265,7 +446,6 @@ export const getGroupedESQuery = ( query: { bool: { filter: [groupedRangeFilter], - ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), }, }, aggregations, @@ -281,7 +461,7 @@ export const getGroupedESQuery = ( }; export const getUngroupedESQuery = ( - params: Omit, + params: Pick & { criteria: Criteria }, timestampField: string, index: string ): object => { @@ -315,7 +495,7 @@ type Filter = { [key in SupportedESQueryTypes]?: object; }; -const buildFiltersForCriteria = (criteria: LogDocumentCountAlertParams['criteria']) => { +const buildFiltersForCriteria = (criteria: Criteria) => { let filters: Filter[] = []; criteria.forEach((criterion) => { @@ -443,7 +623,7 @@ const getGroupedResults = async (query: object, callCluster: AlertServices['call return compositeGroupBuckets; }; -const createConditionsMessage = (criteria: LogDocumentCountAlertParams['criteria']) => { +const createConditionsMessageForCriteria = (criteria: Criteria) => { const parts = criteria.map((criterion, index) => { const { field, comparator, value } = criterion; return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index ab55601f4c47..2c1d7e097660 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; import { PluginSetupContract } from '../../../../../alerts/server'; import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, - Comparator, -} from '../../../../common/alerting/logs/types'; + AlertParamsRT, +} from '../../../../common/alerting/logs/log_threshold/types'; import { InfraBackendLibs } from '../../infra_types'; +import { decodeOrThrow } from '../../../../common/runtime_types'; const documentCountActionVariableDescription = i18n.translate( 'xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription', @@ -34,33 +34,33 @@ const groupByActionVariableDescription = i18n.translate( } ); -const countSchema = schema.object({ - value: schema.number(), - comparator: schema.oneOf([ - schema.literal(Comparator.GT), - schema.literal(Comparator.LT), - schema.literal(Comparator.GT_OR_EQ), - schema.literal(Comparator.LT_OR_EQ), - schema.literal(Comparator.EQ), - ]), -}); +const isRatioActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.isRatioActionVariableDescription', + { + defaultMessage: 'Denotes whether this alert was configured with a ratio', + } +); -const criteriaSchema = schema.object({ - field: schema.string(), - comparator: schema.oneOf([ - schema.literal(Comparator.GT), - schema.literal(Comparator.LT), - schema.literal(Comparator.GT_OR_EQ), - schema.literal(Comparator.LT_OR_EQ), - schema.literal(Comparator.EQ), - schema.literal(Comparator.NOT_EQ), - schema.literal(Comparator.MATCH), - schema.literal(Comparator.NOT_MATCH), - schema.literal(Comparator.MATCH_PHRASE), - schema.literal(Comparator.NOT_MATCH_PHRASE), - ]), - value: schema.oneOf([schema.number(), schema.string()]), -}); +const ratioActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.ratioActionVariableDescription', + { + defaultMessage: 'The ratio value of the two sets of criteria', + } +); + +const numeratorConditionsActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.numeratorConditionsActionVariableDescription', + { + defaultMessage: 'The conditions that the numerator of the ratio needed to fulfill', + } +); + +const denominatorConditionsActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.denominatorConditionsActionVariableDescription', + { + defaultMessage: 'The conditions that the denominator of the ratio needed to fulfill', + } +); export async function registerLogThresholdAlertType( alertingPlugin: PluginSetupContract, @@ -76,13 +76,9 @@ export async function registerLogThresholdAlertType( id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, name: 'Log threshold', validate: { - params: schema.object({ - count: countSchema, - criteria: schema.arrayOf(criteriaSchema), - timeUnit: schema.string(), - timeSize: schema.number(), - groupBy: schema.maybe(schema.arrayOf(schema.string())), - }), + params: { + validate: (params) => decodeOrThrow(AlertParamsRT)(params), + }, }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], @@ -92,6 +88,14 @@ export async function registerLogThresholdAlertType( { name: 'matchingDocuments', description: documentCountActionVariableDescription }, { name: 'conditions', description: conditionsActionVariableDescription }, { name: 'group', description: groupByActionVariableDescription }, + // Ratio alerts + { name: 'isRatio', description: isRatioActionVariableDescription }, + { name: 'ratio', description: ratioActionVariableDescription }, + { name: 'numeratorConditions', description: numeratorConditionsActionVariableDescription }, + { + name: 'denominatorConditions', + description: denominatorConditionsActionVariableDescription, + }, ], }, producer: 'logs', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 79d86a199e90..a26daffcd88d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8319,13 +8319,9 @@ "xpack.infra.logs.alertFlyout.alertName": "ログしきい値", "xpack.infra.logs.alertFlyout.criterionComparatorValueTitle": "比較:値", "xpack.infra.logs.alertFlyout.criterionFieldTitle": "フィールド", - "xpack.infra.logs.alertFlyout.documentCountPrefix": "タイミング", - "xpack.infra.logs.alertFlyout.documentCountSuffix": "{value, plural, one {件発生} other {件発生}}", - "xpack.infra.logs.alertFlyout.documentCountValue": "{value, plural, one {ログエントリ} other {ログエントリ}}", "xpack.infra.logs.alertFlyout.error.criterionComparatorRequired": "コンパレーターが必要です。", "xpack.infra.logs.alertFlyout.error.criterionFieldRequired": "フィールドが必要です。", "xpack.infra.logs.alertFlyout.error.criterionValueRequired": "値が必要です。", - "xpack.infra.logs.alertFlyout.error.documentCountRequired": "ドキュメントカウントが必要です。", "xpack.infra.logs.alertFlyout.error.timeSizeRequired": "ページサイズが必要です。", "xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix": "With", "xpack.infra.logs.alertFlyout.removeCondition": "条件を削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3e45418e28a2..444fff9efa8b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8323,13 +8323,9 @@ "xpack.infra.logs.alertFlyout.alertName": "日志阈值", "xpack.infra.logs.alertFlyout.criterionComparatorValueTitle": "对比:值", "xpack.infra.logs.alertFlyout.criterionFieldTitle": "字段", - "xpack.infra.logs.alertFlyout.documentCountPrefix": "当", - "xpack.infra.logs.alertFlyout.documentCountSuffix": "{value, plural, one {发生} other {发生}}", - "xpack.infra.logs.alertFlyout.documentCountValue": "{value, plural, one {日志条目} other {log 日志条目}}", "xpack.infra.logs.alertFlyout.error.criterionComparatorRequired": "比较运算符必填。", "xpack.infra.logs.alertFlyout.error.criterionFieldRequired": "“字段”必填。", "xpack.infra.logs.alertFlyout.error.criterionValueRequired": "“值”必填。", - "xpack.infra.logs.alertFlyout.error.documentCountRequired": "“文档计数”必填。", "xpack.infra.logs.alertFlyout.error.timeSizeRequired": "“时间大小”必填。", "xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix": "具有", "xpack.infra.logs.alertFlyout.removeCondition": "删除条件",