[Logs UI] Log threshold ratio alerts (#76867)

* Add ratio alerting to log threshold alerts

* Fix i18n

* Move grouped query must not filtering from outer to inner clause

* Use new ratio alerting layout

* Use better defaults for ratio alerts

* Remove div wrapper

* Remove type casting, use user-defined type guards

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Kerry Gallagher 2020-09-29 19:59:18 +01:00 committed by GitHub
parent cb0e650e40
commit 33d051b73c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1135 additions and 547 deletions

View file

@ -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<typeof ThresholdTypeRT>;
// 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<typeof DocumentCountRT>;
export type Threshold = rt.TypeOf<typeof ThresholdRT>;
export const CriterionRT = rt.type({
field: rt.string,
@ -104,6 +113,13 @@ export const CriterionRT = rt.type({
export type Criterion = rt.TypeOf<typeof CriterionRT>;
export const criteriaRT = rt.array(CriterionRT);
export type Criteria = rt.TypeOf<typeof criteriaRT>;
export const countCriteriaRT = criteriaRT;
export type CountCriteria = rt.TypeOf<typeof countCriteriaRT>;
export const ratioCriteriaRT = rt.tuple([criteriaRT, criteriaRT]);
export type RatioCriteria = rt.TypeOf<typeof ratioCriteriaRT>;
export const TimeUnitRT = rt.union([
rt.literal('s'),
@ -116,25 +132,73 @@ export type TimeUnit = rt.TypeOf<typeof TimeUnitRT>;
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<typeof LogDocumentCountAlertParamsRT>;
export type CountAlertParams = rt.TypeOf<typeof alertParamsRT>;
export const ratioAlertParamsRT = rt.intersection([
rt.type({
criteria: ratioCriteriaRT,
...RequiredAlertParamsRT.props,
}),
rt.partial({
...OptionalAlertParamsRT.props,
}),
]);
export type RatioAlertParams = rt.TypeOf<typeof ratioAlertParamsRT>;
export const AlertParamsRT = rt.union([alertParamsRT, ratioAlertParamsRT]);
export type AlertParams = rt.TypeOf<typeof AlertParamsRT>;
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([

View file

@ -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';

View file

@ -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<CriterionType>) => void;
removeCriterion: (idx: number) => void;
errors: IErrorObject;
alertParams: Partial<LogDocumentCountAlertParams>;
context: AlertsContext;
sourceId: string;
}
export const Criteria: React.FC<Props> = ({
fields,
criteria,
updateCriterion,
removeCriterion,
errors,
alertParams,
context,
sourceId,
}) => {
if (!criteria) return null;
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow>
{criteria.map((criterion, idx) => {
return (
<EuiAccordion
id={`criterion-${idx}`}
buttonContent={
<Criterion
idx={idx}
fields={fields}
criterion={criterion}
updateCriterion={updateCriterion}
removeCriterion={removeCriterion}
canDelete={criteria.length > 1}
errors={errors[idx.toString()] as IErrorObject}
/>
}
key={idx}
arrowDisplay="right"
>
<CriterionPreview
alertParams={alertParams}
context={context}
chartCriterion={criterion}
sourceId={sourceId}
/>
</EuiAccordion>
);
})}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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<LogDocumentCountAlertParams['count']>) => void;
errors: IErrorObject;
}
export const DocumentCount: React.FC<Props> = ({ 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 (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiPopover
id="comparator"
button={
<EuiExpression
description={documentCountPrefix}
uppercase={true}
value={comparator ? ComparatorToi18nMap[comparator] : ''}
isActive={isComparatorPopoverOpen}
onClick={() => setComparatorPopoverOpenState(true)}
/>
}
isOpen={isComparatorPopoverOpen}
closePopover={() => setComparatorPopoverOpenState(false)}
ownFocus
panelPaddingSize="s"
anchorPosition="downLeft"
>
<div>
<EuiPopoverTitle>{documentCountPrefix}</EuiPopoverTitle>
<EuiSelect
compressed
value={comparator}
onChange={(e) => updateCount({ comparator: e.target.value as Comparator })}
options={getComparatorOptions()}
/>
</div>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
id="comparator"
button={
<EuiExpression
description={value}
uppercase={true}
value={documentCountValue}
isActive={isValuePopoverOpen}
onClick={() => setIsValuePopoverOpen(true)}
color={errors.value.length === 0 ? 'secondary' : 'danger'}
/>
}
isOpen={isValuePopoverOpen}
closePopover={() => setIsValuePopoverOpen(false)}
ownFocus
panelPaddingSize="s"
anchorPosition="downLeft"
>
<div>
<EuiPopoverTitle>{documentCountValue}</EuiPopoverTitle>
<EuiFormRow isInvalid={errors.value.length > 0} error={errors.value}>
<EuiFieldNumber
compressed
value={value}
onChange={(e) => {
const number = parseInt(e.target.value, 10);
updateCount({ value: number ? number : undefined });
}}
/>
</EuiFormRow>
</div>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiExpression description={documentCountSuffix} value="" />
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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);

View file

@ -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;

View file

@ -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<AlertParams>;
context: AlertsContext;
sourceId: string;
updateCriteria: (criteria: AlertParams['criteria']) => void;
}
type CriteriaProps = SharedProps;
export const Criteria: React.FC<CriteriaProps> = (props) => {
const { criteria, errors } = props;
if (!criteria || criteria.length === 0) return null;
return !isRatioAlert(criteria) ? (
<CountCriteria {...props} criteria={criteria} errors={errors} />
) : (
<RatioCriteria {...props} criteria={criteria} errors={errors} />
);
};
interface CriteriaWrapperProps {
alertParams: SharedProps['alertParams'];
fields: SharedProps['fields'];
updateCriterion: (idx: number, params: Partial<CriterionType>) => void;
removeCriterion: (idx: number) => void;
addCriterion: () => void;
criteria: CriteriaType;
errors: CriterionErrors;
context: SharedProps['context'];
sourceId: SharedProps['sourceId'];
isRatio?: boolean;
}
const CriteriaWrapper: React.FC<CriteriaWrapperProps> = (props) => {
const {
updateCriterion,
removeCriterion,
addCriterion,
criteria,
fields,
errors,
alertParams,
context,
sourceId,
isRatio = false,
} = props;
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow>
{criteria.map((criterion, idx) => {
return (
<EuiAccordion
id={`criterion-${idx}`}
buttonContent={
<Criterion
idx={idx}
fields={fields}
criterion={criterion}
updateCriterion={updateCriterion}
removeCriterion={removeCriterion}
canDelete={criteria.length > 1}
errors={errors[idx]}
/>
}
key={idx}
arrowDisplay="right"
>
<CriterionPreview
alertParams={alertParams}
context={context}
chartCriterion={criterion}
sourceId={sourceId}
showThreshold={!isRatio}
/>
</EuiAccordion>
);
})}
<AddCriterionButton addCriterion={addCriterion} />
</EuiFlexItem>
</EuiFlexGroup>
);
};
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<RatioCriteriaProps> = (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 (
<>
<EuiSpacer size="xxl" />
<ExpressionLike text={QueryAText} />
<CriteriaWrapper
{...props}
criteria={getNumerator(criteria)}
updateCriterion={updateNumeratorCriterion}
addCriterion={addNumeratorCriterion}
removeCriterion={removeNumeratorCriterion}
errors={errors[0]}
isRatio={true}
/>
<EuiSpacer size="l" />
<ExpressionLike text={QueryBText} />
<CriteriaWrapper
{...props}
criteria={getDenominator(criteria)}
updateCriterion={updateDenominatorCriterion}
addCriterion={addDenominatorCriterion}
removeCriterion={removeDenominatorCriterion}
errors={errors[1]}
isRatio={true}
/>
</>
);
};
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<CountCriteriaProps> = (props) => {
const { criteria, updateCriteria, errors } = props;
const handleUpdateCriteria = useCallback(
(criteriaParam: CriteriaType) => {
updateCriteria(criteriaParam);
},
[updateCriteria]
);
const { updateCriterion, addCriterion, removeCriterion } = useCriteriaState(
criteria,
handleUpdateCriteria
);
return (
<CriteriaWrapper
{...props}
updateCriterion={updateCriterion}
addCriterion={addCriterion}
removeCriterion={removeCriterion}
errors={errors[0]}
/>
);
};
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 (
<div>
<EuiButtonEmpty
color={'primary'}
iconSide={'left'}
flush={'left'}
iconType={'plusInCircleFilled'}
onClick={addCriterion}
>
<FormattedMessage
id="xpack.infra.logs.alertFlyout.addCondition"
defaultMessage="Add condition"
/>
</EuiButtonEmpty>
</div>
);
};

View file

@ -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<Props> = ({
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,
});
}}
/>
) : (

View file

@ -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<LogDocumentCountAlertParams>;
alertParams: Partial<AlertParams>;
context: AlertsContext;
chartCriterion: Partial<Criterion>;
sourceId: string;
showThreshold: boolean;
}
export const CriterionPreview: React.FC<Props> = ({
@ -60,6 +62,7 @@ export const CriterionPreview: React.FC<Props> = ({
context,
chartCriterion,
sourceId,
showThreshold,
}) => {
const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => {
const { field, comparator, value } = chartCriterion;
@ -92,6 +95,7 @@ export const CriterionPreview: React.FC<Props> = ({
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<ChartProps> = ({
@ -110,6 +115,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
sourceId,
threshold,
chartAlertParams,
showThreshold,
}) => {
const isDarkMode = context.uiSettings?.get('theme:darkMode') || false;
@ -140,17 +146,18 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
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<ChartProps> = ({
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<ChartProps> = ({
}}
color={!isGrouped ? colorTransformer(Color.color0) : undefined}
/>
{threshold && threshold.value ? (
{showThreshold && threshold && threshold.value ? (
<LineAnnotation
id={`threshold-line`}
domainType={AnnotationDomainTypes.YDomain}
@ -243,7 +253,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
}}
/>
) : null}
{threshold && threshold.value && isBelow ? (
{showThreshold && threshold && threshold.value && isBelow ? (
<RectAnnotation
id="below-threshold"
style={{
@ -262,7 +272,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
]}
/>
) : null}
{threshold && threshold.value && isAbove ? (
{showThreshold && threshold && threshold.value && isAbove ? (
<RectAnnotation
id="above-threshold"
style={{

View file

@ -6,23 +6,27 @@
import React, { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui';
import { EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui';
import { useMount } from 'react-use';
import { FormattedMessage } from '@kbn/i18n/react';
import {
ForLastExpression,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../triggers_actions_ui/public/common';
} from '../../../../../../../triggers_actions_ui/public/common';
// 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';
import { LogDocumentCountAlertParams, Comparator } from '../../../../../common/alerting/logs/types';
import { DocumentCount } from './document_count';
import { AlertsContextValue } from '../../../../../../../triggers_actions_ui/public/application/context/alerts_context';
import {
AlertParams,
Comparator,
ThresholdType,
isRatioAlert,
} from '../../../../../../common/alerting/logs/log_threshold/types';
import { Threshold } from './threshold';
import { Criteria } from './criteria';
import { useSourceId } from '../../../../containers/source_id';
import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source';
import { GroupByExpression } from '../../shared/group_by_expression/group_by_expression';
import { TypeSwitcher } from './type_switcher';
import { useSourceId } from '../../../../../containers/source_id';
import { LogSourceProvider, useLogSourceContext } from '../../../../../containers/logs/log_source';
import { GroupByExpression } from '../../../shared/group_by_expression/group_by_expression';
import { Errors } from '../validation';
export interface ExpressionCriteria {
field?: string;
@ -36,8 +40,8 @@ interface LogsContextMeta {
export type AlertsContext = AlertsContextValue<LogsContextMeta>;
interface Props {
errors: IErrorObject;
alertParams: Partial<LogDocumentCountAlertParams>;
errors: Errors;
alertParams: Partial<AlertParams>;
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> = (props) => {
@ -125,10 +145,10 @@ export const Editor: React.FC<Props> = (props) => {
const [hasSetDefaults, setHasSetDefaults] = useState<boolean>(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> = (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> = (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 ? (
<Criteria
fields={supportedFields}
criteria={alertParams.criteria}
errors={errors.criteria}
alertParams={alertParams}
context={alertsContext}
sourceId={sourceId}
updateCriteria={updateCriteria}
/>
) : null;
return (
<>
<DocumentCount
<TypeSwitcher criteria={alertParams.criteria || []} updateType={updateType} />
{alertParams.criteria && !isRatioAlert(alertParams.criteria) && criteriaComponent}
<Threshold
comparator={alertParams.count?.comparator}
value={alertParams.count?.value}
updateCount={updateCount}
errors={errors.count as IErrorObject}
/>
<Criteria
fields={supportedFields}
criteria={alertParams.criteria}
updateCriterion={updateCriterion}
removeCriterion={removeCriterion}
errors={errors.criteria as IErrorObject}
alertParams={alertParams}
context={alertsContext}
sourceId={sourceId}
updateThreshold={updateThreshold}
errors={errors.threshold}
/>
<ForLastExpression
@ -239,7 +256,7 @@ export const Editor: React.FC<Props> = (props) => {
timeWindowUnit={alertParams.timeUnit}
onChangeWindowSize={updateTimeSize}
onChangeWindowUnit={updateTimeUnit}
errors={errors as { [key: string]: string[] }}
errors={{ timeWindowSize: errors.timeWindowSize, timeSizeUnit: errors.timeSizeUnit }}
/>
<GroupByExpression
@ -248,20 +265,9 @@ export const Editor: React.FC<Props> = (props) => {
fields={groupByFields}
/>
<div>
<EuiButtonEmpty
color={'primary'}
iconSide={'left'}
flush={'left'}
iconType={'plusInCircleFilled'}
onClick={addCriterion}
>
<FormattedMessage
id="xpack.infra.logs.alertFlyout.addCondition"
defaultMessage="Add condition"
/>
</EuiButtonEmpty>
</div>
{alertParams.criteria && isRatioAlert(alertParams.criteria) && criteriaComponent}
<EuiSpacer size="l" />
</>
);
};
@ -269,3 +275,13 @@ export const Editor: React.FC<Props> = (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 (
<div className="euiExpression euiExpression-isUppercase euiExpression--secondary">
<span className="euiExpression__description">{text}</span>
</div>
);
};

View file

@ -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;

View file

@ -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<AlertParams['count']>) => void;
errors: IErrorObject;
}
export const Threshold: React.FC<Props> = ({ comparator, value, updateThreshold, errors }) => {
const [isThresholdPopoverOpen, setThresholdPopoverOpenState] = useState(false);
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiPopover
id="threshold"
button={
<EuiExpression
description={thresholdPrefix}
uppercase={true}
value={`${comparator ? ComparatorToi18nMap[comparator] : ''} ${value ? value : ''}`}
isActive={isThresholdPopoverOpen}
onClick={() => setThresholdPopoverOpenState(true)}
/>
}
isOpen={isThresholdPopoverOpen}
closePopover={() => setThresholdPopoverOpenState(false)}
ownFocus
panelPaddingSize="s"
anchorPosition="downLeft"
>
<>
<EuiPopoverTitle>{popoverTitle}</EuiPopoverTitle>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiFormRow>
<EuiSelect
compressed
value={comparator}
onChange={(e) => updateThreshold({ comparator: e.target.value as Comparator })}
options={getComparatorOptions()}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow isInvalid={errors.value.length > 0} error={errors.value}>
<EuiFieldNumber
compressed
value={value}
onChange={(e) => {
const number = parseFloat(e.target.value);
updateThreshold({
value: isNumber(number) && isFinite(number) ? number : undefined,
});
}}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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<Props> = ({ criteria, updateType }) => {
const [isThresholdTypePopoverOpen, setThresholdTypePopoverOpenState] = useState(false);
const thresholdType = getThresholdType(criteria);
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiPopover
id="thresholdType"
button={
<>
<EuiExpression
description={typePrefix}
uppercase={true}
value={thresholdType === 'ratio' ? ratioI18n : countI18n}
isActive={isThresholdTypePopoverOpen}
onClick={() => setThresholdTypePopoverOpenState(true)}
/>
<ExpressionLike
text={
thresholdType === 'ratio' ? ratioSuffix.toUpperCase() : countSuffix.toUpperCase()
}
/>
</>
}
isOpen={isThresholdTypePopoverOpen}
closePopover={() => setThresholdTypePopoverOpenState(false)}
ownFocus
panelPaddingSize="s"
anchorPosition="downLeft"
>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiSelect
compressed
value={thresholdType}
onChange={(e) => updateType(thresholdType === 'ratio' ? 'count' : 'ratio')}
options={getOptions()}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -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,

View file

@ -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<AlertParams>): 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;
}

View file

@ -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<LogDocumentCountAlertParams>): 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;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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';

View file

@ -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;

View file

@ -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<LogDocumentCountAlertParams, 'count' | 'timeSize' | 'timeUnit'> = {
const baseAlertParams: Pick<AlertParams, 'count' | 'timeSize' | 'timeUnit'> = {
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,
},
},
]);

View file

@ -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<AlertParams, 'criteria'> & { 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<ReducedGroupByResults[]>((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<ReducedGroupByResults>((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<LogDocumentCountAlertParams, 'count'>,
params: Pick<AlertParams, 'timeSize' | 'timeUnit'> & { criteria: Criteria },
timestampField: string
) => {
const { timeSize, timeUnit, criteria } = params;
@ -223,7 +403,7 @@ export const buildFiltersFromCriteria = (
};
export const getGroupedESQuery = (
params: Omit<LogDocumentCountAlertParams, 'count'>,
params: Pick<AlertParams, 'timeSize' | 'timeUnit' | 'groupBy'> & { 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<LogDocumentCountAlertParams, 'count'>,
params: Pick<AlertParams, 'timeSize' | 'timeUnit'> & { 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}`;

View file

@ -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',

View file

@ -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": "条件を削除",

View file

@ -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": "删除条件",