[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:
parent
cb0e650e40
commit
33d051b73c
|
@ -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([
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
|
@ -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={{
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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": "条件を削除",
|
||||
|
|
|
@ -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": "删除条件",
|
||||
|
|
Loading…
Reference in a new issue