[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 { i18n } from '@kbn/i18n';
|
||||||
import * as rt from 'io-ts';
|
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';
|
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 {
|
export enum Comparator {
|
||||||
GT = 'more than',
|
GT = 'more than',
|
||||||
GT_OR_EQ = 'more than or equals',
|
GT_OR_EQ = 'more than or equals',
|
||||||
|
@ -82,6 +90,7 @@ export const ComparatorToi18nMap = {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Alert parameters //
|
||||||
export enum AlertStates {
|
export enum AlertStates {
|
||||||
OK,
|
OK,
|
||||||
ALERT,
|
ALERT,
|
||||||
|
@ -89,12 +98,12 @@ export enum AlertStates {
|
||||||
ERROR,
|
ERROR,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DocumentCountRT = rt.type({
|
const ThresholdRT = rt.type({
|
||||||
comparator: ComparatorRT,
|
comparator: ComparatorRT,
|
||||||
value: rt.number,
|
value: rt.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type DocumentCount = rt.TypeOf<typeof DocumentCountRT>;
|
export type Threshold = rt.TypeOf<typeof ThresholdRT>;
|
||||||
|
|
||||||
export const CriterionRT = rt.type({
|
export const CriterionRT = rt.type({
|
||||||
field: rt.string,
|
field: rt.string,
|
||||||
|
@ -104,6 +113,13 @@ export const CriterionRT = rt.type({
|
||||||
|
|
||||||
export type Criterion = rt.TypeOf<typeof CriterionRT>;
|
export type Criterion = rt.TypeOf<typeof CriterionRT>;
|
||||||
export const criteriaRT = rt.array(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([
|
export const TimeUnitRT = rt.union([
|
||||||
rt.literal('s'),
|
rt.literal('s'),
|
||||||
|
@ -116,25 +132,73 @@ export type TimeUnit = rt.TypeOf<typeof TimeUnitRT>;
|
||||||
export const timeSizeRT = rt.number;
|
export const timeSizeRT = rt.number;
|
||||||
export const groupByRT = rt.array(rt.string);
|
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({
|
rt.type({
|
||||||
count: DocumentCountRT,
|
criteria: countCriteriaRT,
|
||||||
criteria: criteriaRT,
|
...RequiredAlertParamsRT.props,
|
||||||
timeUnit: TimeUnitRT,
|
|
||||||
timeSize: timeSizeRT,
|
|
||||||
}),
|
}),
|
||||||
rt.partial({
|
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({
|
const chartPreviewHistogramBucket = rt.type({
|
||||||
key: rt.number,
|
key: rt.number,
|
||||||
doc_count: rt.number,
|
doc_count: rt.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ES query responses //
|
||||||
export const UngroupedSearchQueryResponseRT = rt.intersection([
|
export const UngroupedSearchQueryResponseRT = rt.intersection([
|
||||||
commonSearchSuccessResponseFieldsRT,
|
commonSearchSuccessResponseFieldsRT,
|
||||||
rt.intersection([
|
rt.intersection([
|
|
@ -5,7 +5,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as rt from 'io-ts';
|
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';
|
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 { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { AlertFlyout } from './alert_flyout';
|
import { AlertFlyout } from './alert_flyout';
|
||||||
import { useLinkProps } from '../../../hooks/use_link_props';
|
import { useLinkProps } from '../../../../hooks/use_link_props';
|
||||||
|
|
||||||
export const AlertDropdown = () => {
|
export const AlertDropdown = () => {
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
|
@ -6,11 +6,10 @@
|
||||||
|
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public';
|
import { ApplicationStart, DocLinksStart, HttpStart, NotificationsStart } from 'src/core/public';
|
||||||
|
import { AlertsContextProvider, AlertAdd } from '../../../../../../triggers_actions_ui/public';
|
||||||
import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public';
|
import { TriggerActionsContext } from '../../../../utils/triggers_actions_context';
|
||||||
import { TriggerActionsContext } from '../../../utils/triggers_actions_context';
|
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../../common/alerting/logs/log_threshold/types';
|
||||||
import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/types';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible?: boolean;
|
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,
|
EuiFormRow,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { isNumber, isFinite } from 'lodash';
|
||||||
import { IFieldType } from 'src/plugins/data/public';
|
import { IFieldType } from 'src/plugins/data/public';
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
// 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 {
|
import {
|
||||||
Comparator,
|
Comparator,
|
||||||
Criterion as CriterionType,
|
Criterion as CriterionType,
|
||||||
ComparatorToi18nMap,
|
ComparatorToi18nMap,
|
||||||
} from '../../../../../common/alerting/logs/types';
|
} from '../../../../../../common/alerting/logs/log_threshold/types';
|
||||||
|
|
||||||
const firstCriterionFieldPrefix = i18n.translate(
|
const firstCriterionFieldPrefix = i18n.translate(
|
||||||
'xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix',
|
'xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix',
|
||||||
|
@ -239,8 +240,10 @@ export const Criterion: React.FC<Props> = ({
|
||||||
compressed
|
compressed
|
||||||
value={criterion.value as number | undefined}
|
value={criterion.value as number | undefined}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const number = parseInt(e.target.value, 10);
|
const number = parseFloat(e.target.value);
|
||||||
updateCriterion(idx, { value: number ? number : undefined });
|
updateCriterion(idx, {
|
||||||
|
value: isNumber(number) && isFinite(number) ? number : undefined,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
|
@ -31,28 +31,30 @@ import {
|
||||||
getChartTheme,
|
getChartTheme,
|
||||||
yAxisFormatter,
|
yAxisFormatter,
|
||||||
NUM_BUCKETS,
|
NUM_BUCKETS,
|
||||||
} from '../../shared/criterion_preview_chart/criterion_preview_chart';
|
} from '../../../shared/criterion_preview_chart/criterion_preview_chart';
|
||||||
import {
|
import {
|
||||||
LogDocumentCountAlertParams,
|
AlertParams,
|
||||||
|
Threshold,
|
||||||
Criterion,
|
Criterion,
|
||||||
Comparator,
|
Comparator,
|
||||||
} from '../../../../../common/alerting/logs/types';
|
} from '../../../../../../common/alerting/logs/log_threshold/types';
|
||||||
import { Color, colorTransformer } from '../../../../../common/color_palette';
|
import { Color, colorTransformer } from '../../../../../../common/color_palette';
|
||||||
import {
|
import {
|
||||||
GetLogAlertsChartPreviewDataAlertParamsSubset,
|
GetLogAlertsChartPreviewDataAlertParamsSubset,
|
||||||
getLogAlertsChartPreviewDataAlertParamsSubsetRT,
|
getLogAlertsChartPreviewDataAlertParamsSubsetRT,
|
||||||
} from '../../../../../common/http_api/log_alerts/';
|
} from '../../../../../../common/http_api/log_alerts/';
|
||||||
import { AlertsContext } from './editor';
|
import { AlertsContext } from './editor';
|
||||||
import { useChartPreviewData } from './hooks/use_chart_preview_data';
|
import { useChartPreviewData } from './hooks/use_chart_preview_data';
|
||||||
import { decodeOrThrow } from '../../../../../common/runtime_types';
|
import { decodeOrThrow } from '../../../../../../common/runtime_types';
|
||||||
|
|
||||||
const GROUP_LIMIT = 5;
|
const GROUP_LIMIT = 5;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
alertParams: Partial<LogDocumentCountAlertParams>;
|
alertParams: Partial<AlertParams>;
|
||||||
context: AlertsContext;
|
context: AlertsContext;
|
||||||
chartCriterion: Partial<Criterion>;
|
chartCriterion: Partial<Criterion>;
|
||||||
sourceId: string;
|
sourceId: string;
|
||||||
|
showThreshold: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CriterionPreview: React.FC<Props> = ({
|
export const CriterionPreview: React.FC<Props> = ({
|
||||||
|
@ -60,6 +62,7 @@ export const CriterionPreview: React.FC<Props> = ({
|
||||||
context,
|
context,
|
||||||
chartCriterion,
|
chartCriterion,
|
||||||
sourceId,
|
sourceId,
|
||||||
|
showThreshold,
|
||||||
}) => {
|
}) => {
|
||||||
const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => {
|
const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => {
|
||||||
const { field, comparator, value } = chartCriterion;
|
const { field, comparator, value } = chartCriterion;
|
||||||
|
@ -92,6 +95,7 @@ export const CriterionPreview: React.FC<Props> = ({
|
||||||
sourceId={sourceId}
|
sourceId={sourceId}
|
||||||
threshold={alertParams.count}
|
threshold={alertParams.count}
|
||||||
chartAlertParams={chartAlertParams}
|
chartAlertParams={chartAlertParams}
|
||||||
|
showThreshold={showThreshold}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -100,8 +104,9 @@ interface ChartProps {
|
||||||
buckets: number;
|
buckets: number;
|
||||||
context: AlertsContext;
|
context: AlertsContext;
|
||||||
sourceId: string;
|
sourceId: string;
|
||||||
threshold?: LogDocumentCountAlertParams['count'];
|
threshold?: Threshold;
|
||||||
chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset;
|
chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset;
|
||||||
|
showThreshold: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CriterionPreviewChart: React.FC<ChartProps> = ({
|
const CriterionPreviewChart: React.FC<ChartProps> = ({
|
||||||
|
@ -110,6 +115,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
|
||||||
sourceId,
|
sourceId,
|
||||||
threshold,
|
threshold,
|
||||||
chartAlertParams,
|
chartAlertParams,
|
||||||
|
showThreshold,
|
||||||
}) => {
|
}) => {
|
||||||
const isDarkMode = context.uiSettings?.get('theme:darkMode') || false;
|
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 isGrouped = groupBy && groupBy.length > 0 ? true : false;
|
||||||
|
|
||||||
const isAbove =
|
const isAbove =
|
||||||
threshold && threshold.comparator
|
showThreshold && threshold && threshold.comparator
|
||||||
? [Comparator.GT, Comparator.GT_OR_EQ].includes(threshold.comparator)
|
? [Comparator.GT, Comparator.GT_OR_EQ].includes(threshold.comparator)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const isBelow =
|
const isBelow =
|
||||||
threshold && threshold.comparator
|
showThreshold && threshold && threshold.comparator
|
||||||
? [Comparator.LT, Comparator.LT_OR_EQ].includes(threshold.comparator)
|
? [Comparator.LT, Comparator.LT_OR_EQ].includes(threshold.comparator)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
// For grouped scenarios we want to limit the groups displayed, for "isAbove" thresholds we'll show
|
// 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.
|
// 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(() => {
|
const filteredSeries = useMemo(() => {
|
||||||
if (!isGrouped) {
|
if (!isGrouped) {
|
||||||
return series;
|
return series;
|
||||||
|
@ -183,11 +190,14 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
|
||||||
const hasData = series.length > 0;
|
const hasData = series.length > 0;
|
||||||
const { yMin, yMax, xMin, xMax } = getDomain(filteredSeries, isStacked);
|
const { yMin, yMax, xMin, xMax } = getDomain(filteredSeries, isStacked);
|
||||||
const chartDomain = {
|
const chartDomain = {
|
||||||
max: threshold && threshold.value ? Math.max(yMax, threshold.value) * 1.1 : yMax * 1.1, // Add 10% headroom.
|
max:
|
||||||
min: threshold && threshold.value ? Math.min(yMin, threshold.value) : yMin,
|
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
|
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}
|
color={!isGrouped ? colorTransformer(Color.color0) : undefined}
|
||||||
/>
|
/>
|
||||||
{threshold && threshold.value ? (
|
{showThreshold && threshold && threshold.value ? (
|
||||||
<LineAnnotation
|
<LineAnnotation
|
||||||
id={`threshold-line`}
|
id={`threshold-line`}
|
||||||
domainType={AnnotationDomainTypes.YDomain}
|
domainType={AnnotationDomainTypes.YDomain}
|
||||||
|
@ -243,7 +253,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{threshold && threshold.value && isBelow ? (
|
{showThreshold && threshold && threshold.value && isBelow ? (
|
||||||
<RectAnnotation
|
<RectAnnotation
|
||||||
id="below-threshold"
|
id="below-threshold"
|
||||||
style={{
|
style={{
|
||||||
|
@ -262,7 +272,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{threshold && threshold.value && isAbove ? (
|
{showThreshold && threshold && threshold.value && isAbove ? (
|
||||||
<RectAnnotation
|
<RectAnnotation
|
||||||
id="above-threshold"
|
id="above-threshold"
|
||||||
style={{
|
style={{
|
|
@ -6,23 +6,27 @@
|
||||||
|
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
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 { useMount } from 'react-use';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
|
||||||
import {
|
import {
|
||||||
ForLastExpression,
|
ForLastExpression,
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
// 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
|
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||||
import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types';
|
import { AlertsContextValue } from '../../../../../../../triggers_actions_ui/public/application/context/alerts_context';
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
import {
|
||||||
import { AlertsContextValue } from '../../../../../../triggers_actions_ui/public/application/context/alerts_context';
|
AlertParams,
|
||||||
import { LogDocumentCountAlertParams, Comparator } from '../../../../../common/alerting/logs/types';
|
Comparator,
|
||||||
import { DocumentCount } from './document_count';
|
ThresholdType,
|
||||||
|
isRatioAlert,
|
||||||
|
} from '../../../../../../common/alerting/logs/log_threshold/types';
|
||||||
|
import { Threshold } from './threshold';
|
||||||
import { Criteria } from './criteria';
|
import { Criteria } from './criteria';
|
||||||
import { useSourceId } from '../../../../containers/source_id';
|
import { TypeSwitcher } from './type_switcher';
|
||||||
import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source';
|
import { useSourceId } from '../../../../../containers/source_id';
|
||||||
import { GroupByExpression } from '../../shared/group_by_expression/group_by_expression';
|
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 {
|
export interface ExpressionCriteria {
|
||||||
field?: string;
|
field?: string;
|
||||||
|
@ -36,8 +40,8 @@ interface LogsContextMeta {
|
||||||
|
|
||||||
export type AlertsContext = AlertsContextValue<LogsContextMeta>;
|
export type AlertsContext = AlertsContextValue<LogsContextMeta>;
|
||||||
interface Props {
|
interface Props {
|
||||||
errors: IErrorObject;
|
errors: Errors;
|
||||||
alertParams: Partial<LogDocumentCountAlertParams>;
|
alertParams: Partial<AlertParams>;
|
||||||
setAlertParams(key: string, value: any): void;
|
setAlertParams(key: string, value: any): void;
|
||||||
setAlertProperty(key: string, value: any): void;
|
setAlertProperty(key: string, value: any): void;
|
||||||
alertsContext: AlertsContext;
|
alertsContext: AlertsContext;
|
||||||
|
@ -46,14 +50,30 @@ interface Props {
|
||||||
|
|
||||||
const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' };
|
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: {
|
count: {
|
||||||
value: 75,
|
value: 75,
|
||||||
comparator: Comparator.GT,
|
comparator: Comparator.GT,
|
||||||
},
|
},
|
||||||
criteria: [DEFAULT_CRITERIA],
|
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) => {
|
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 [hasSetDefaults, setHasSetDefaults] = useState<boolean>(false);
|
||||||
const { sourceStatus } = useLogSourceContext();
|
const { sourceStatus } = useLogSourceContext();
|
||||||
useMount(() => {
|
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);
|
setAlertParams(key, value);
|
||||||
setHasSetDefaults(true);
|
|
||||||
}
|
}
|
||||||
|
setHasSetDefaults(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const supportedFields = useMemo(() => {
|
const supportedFields = useMemo(() => {
|
||||||
|
@ -153,22 +173,19 @@ export const Editor: React.FC<Props> = (props) => {
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
}, [sourceStatus]);
|
}, [sourceStatus]);
|
||||||
|
|
||||||
const updateCount = useCallback(
|
const updateThreshold = useCallback(
|
||||||
(countParams) => {
|
(thresholdParams) => {
|
||||||
const nextCountParams = { ...alertParams.count, ...countParams };
|
const nextThresholdParams = { ...alertParams.count, ...thresholdParams };
|
||||||
setAlertParams('count', nextCountParams);
|
setAlertParams('count', nextThresholdParams);
|
||||||
},
|
},
|
||||||
[alertParams.count, setAlertParams]
|
[alertParams.count, setAlertParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateCriterion = useCallback(
|
const updateCriteria = useCallback(
|
||||||
(idx, criterionParams) => {
|
(criteria: AlertParams['criteria']) => {
|
||||||
const nextCriteria = alertParams.criteria?.map((criterion, index) => {
|
setAlertParams('criteria', criteria);
|
||||||
return idx === index ? { ...criterion, ...criterionParams } : criterion;
|
|
||||||
});
|
|
||||||
setAlertParams('criteria', nextCriteria ? nextCriteria : []);
|
|
||||||
},
|
},
|
||||||
[alertParams, setAlertParams]
|
[setAlertParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateTimeSize = useCallback(
|
const updateTimeSize = useCallback(
|
||||||
|
@ -192,46 +209,46 @@ export const Editor: React.FC<Props> = (props) => {
|
||||||
[setAlertParams]
|
[setAlertParams]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addCriterion = useCallback(() => {
|
const updateType = useCallback(
|
||||||
const nextCriteria = alertParams?.criteria
|
(type: ThresholdType) => {
|
||||||
? [...alertParams.criteria, DEFAULT_CRITERIA]
|
const defaults = type === 'count' ? DEFAULT_COUNT_EXPRESSION : DEFAULT_RATIO_EXPRESSION;
|
||||||
: [DEFAULT_CRITERIA];
|
// Reset properties that don't make sense switching from one context to the other
|
||||||
setAlertParams('criteria', nextCriteria);
|
for (const [key, value] of Object.entries({
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
criteria: defaults.criteria,
|
||||||
}, [alertParams, setAlertParams]);
|
count: defaults.count,
|
||||||
|
})) {
|
||||||
const removeCriterion = useCallback(
|
setAlertParams(key, value);
|
||||||
(idx) => {
|
}
|
||||||
const nextCriteria = alertParams?.criteria?.filter((criterion, index) => {
|
|
||||||
return index !== idx;
|
|
||||||
});
|
|
||||||
setAlertParams('criteria', nextCriteria);
|
|
||||||
},
|
},
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
[setAlertParams]
|
||||||
[alertParams, setAlertParams]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait until the alert param defaults have been set
|
// Wait until the alert param defaults have been set
|
||||||
if (!hasSetDefaults) return null;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentCount
|
<TypeSwitcher criteria={alertParams.criteria || []} updateType={updateType} />
|
||||||
|
|
||||||
|
{alertParams.criteria && !isRatioAlert(alertParams.criteria) && criteriaComponent}
|
||||||
|
|
||||||
|
<Threshold
|
||||||
comparator={alertParams.count?.comparator}
|
comparator={alertParams.count?.comparator}
|
||||||
value={alertParams.count?.value}
|
value={alertParams.count?.value}
|
||||||
updateCount={updateCount}
|
updateThreshold={updateThreshold}
|
||||||
errors={errors.count as IErrorObject}
|
errors={errors.threshold}
|
||||||
/>
|
|
||||||
|
|
||||||
<Criteria
|
|
||||||
fields={supportedFields}
|
|
||||||
criteria={alertParams.criteria}
|
|
||||||
updateCriterion={updateCriterion}
|
|
||||||
removeCriterion={removeCriterion}
|
|
||||||
errors={errors.criteria as IErrorObject}
|
|
||||||
alertParams={alertParams}
|
|
||||||
context={alertsContext}
|
|
||||||
sourceId={sourceId}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ForLastExpression
|
<ForLastExpression
|
||||||
|
@ -239,7 +256,7 @@ export const Editor: React.FC<Props> = (props) => {
|
||||||
timeWindowUnit={alertParams.timeUnit}
|
timeWindowUnit={alertParams.timeUnit}
|
||||||
onChangeWindowSize={updateTimeSize}
|
onChangeWindowSize={updateTimeSize}
|
||||||
onChangeWindowUnit={updateTimeUnit}
|
onChangeWindowUnit={updateTimeUnit}
|
||||||
errors={errors as { [key: string]: string[] }}
|
errors={{ timeWindowSize: errors.timeWindowSize, timeSizeUnit: errors.timeSizeUnit }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GroupByExpression
|
<GroupByExpression
|
||||||
|
@ -248,20 +265,9 @@ export const Editor: React.FC<Props> = (props) => {
|
||||||
fields={groupByFields}
|
fields={groupByFields}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
{alertParams.criteria && isRatioAlert(alertParams.criteria) && criteriaComponent}
|
||||||
<EuiButtonEmpty
|
|
||||||
color={'primary'}
|
<EuiSpacer size="l" />
|
||||||
iconSide={'left'}
|
|
||||||
flush={'left'}
|
|
||||||
iconType={'plusInCircleFilled'}
|
|
||||||
onClick={addCriterion}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id="xpack.infra.logs.alertFlyout.addCondition"
|
|
||||||
defaultMessage="Add condition"
|
|
||||||
/>
|
|
||||||
</EuiButtonEmpty>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -269,3 +275,13 @@ export const Editor: React.FC<Props> = (props) => {
|
||||||
// required for dynamic import
|
// required for dynamic import
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default ExpressionEditor;
|
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 { useState, useMemo } from 'react';
|
||||||
import { AlertsContext } from '../editor';
|
import { AlertsContext } from '../editor';
|
||||||
import { useTrackedPromise } from '../../../../../utils/use_tracked_promise';
|
import { useTrackedPromise } from '../../../../../../utils/use_tracked_promise';
|
||||||
import {
|
import {
|
||||||
GetLogAlertsChartPreviewDataSuccessResponsePayload,
|
GetLogAlertsChartPreviewDataSuccessResponsePayload,
|
||||||
getLogAlertsChartPreviewDataSuccessResponsePayloadRT,
|
getLogAlertsChartPreviewDataSuccessResponsePayloadRT,
|
||||||
getLogAlertsChartPreviewDataRequestPayloadRT,
|
getLogAlertsChartPreviewDataRequestPayloadRT,
|
||||||
LOG_ALERTS_CHART_PREVIEW_DATA_PATH,
|
LOG_ALERTS_CHART_PREVIEW_DATA_PATH,
|
||||||
} from '../../../../../../common/http_api';
|
} from '../../../../../../../common/http_api';
|
||||||
import { decodeOrThrow } from '../../../../../../common/runtime_types';
|
import { decodeOrThrow } from '../../../../../../../common/runtime_types';
|
||||||
import { GetLogAlertsChartPreviewDataAlertParamsSubset } from '../../../../../../common/http_api/log_alerts/';
|
import { GetLogAlertsChartPreviewDataAlertParamsSubset } from '../../../../../../../common/http_api/log_alerts/';
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
sourceId: string;
|
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 { i18n } from '@kbn/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||||
import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types';
|
import { AlertTypeModel } from '../../../../../../triggers_actions_ui/public/types';
|
||||||
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 { validateExpression } from './validation';
|
import { validateExpression } from './validation';
|
||||||
|
|
||||||
export function getAlertType(): AlertTypeModel {
|
export function getAlertType(): AlertTypeModel {
|
||||||
|
@ -22,7 +22,7 @@ export function getAlertType(): AlertTypeModel {
|
||||||
defaultActionMessage: i18n.translate(
|
defaultActionMessage: i18n.translate(
|
||||||
'xpack.infra.logs.alerting.threshold.defaultActionMessage',
|
'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,
|
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 { LogEntryRatePage } from './log_entry_rate';
|
||||||
import { LogsSettingsPage } from './settings';
|
import { LogsSettingsPage } from './settings';
|
||||||
import { StreamPage } from './stream';
|
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 = () => {
|
export const LogsPageContent: React.FunctionComponent = () => {
|
||||||
const uiCapabilities = useKibana().services.application?.capabilities;
|
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 { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
|
||||||
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
|
import { createMetricThresholdAlertType } from './alerting/metric_threshold';
|
||||||
import { createInventoryMetricAlertType } from './alerting/inventory';
|
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 { registerStartSingleton } from './legacy_singletons';
|
||||||
import { registerFeatures } from './register_feature';
|
import { registerFeatures } from './register_feature';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
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_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 { METRIC_THRESHOLD_ALERT_TYPE_ID } from './lib/alerting/metric_threshold/types';
|
||||||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
UngroupedSearchQueryResponse,
|
UngroupedSearchQueryResponse,
|
||||||
GroupedSearchQueryResponse,
|
GroupedSearchQueryResponse,
|
||||||
GroupedSearchQueryResponseRT,
|
GroupedSearchQueryResponseRT,
|
||||||
} from '../../../../common/alerting/logs/types';
|
} from '../../../../common/alerting/logs/log_threshold/types';
|
||||||
import { decodeOrThrow } from '../../../../common/runtime_types';
|
import { decodeOrThrow } from '../../../../common/runtime_types';
|
||||||
|
|
||||||
const COMPOSITE_GROUP_SIZE = 40;
|
const COMPOSITE_GROUP_SIZE = 40;
|
||||||
|
|
|
@ -17,11 +17,11 @@ import {
|
||||||
import {
|
import {
|
||||||
Comparator,
|
Comparator,
|
||||||
AlertStates,
|
AlertStates,
|
||||||
LogDocumentCountAlertParams,
|
AlertParams,
|
||||||
Criterion,
|
Criterion,
|
||||||
UngroupedSearchQueryResponse,
|
UngroupedSearchQueryResponse,
|
||||||
GroupedSearchQueryResponse,
|
GroupedSearchQueryResponse,
|
||||||
} from '../../../../common/alerting/logs/types';
|
} from '../../../../common/alerting/logs/log_threshold/types';
|
||||||
import { alertsMock } from '../../../../../alerts/server/mocks';
|
import { alertsMock } from '../../../../../alerts/server/mocks';
|
||||||
|
|
||||||
// Mocks //
|
// Mocks //
|
||||||
|
@ -56,7 +56,7 @@ const negativeCriteria: Criterion[] = [
|
||||||
{ ...textField, comparator: Comparator.NOT_MATCH_PHRASE },
|
{ ...textField, comparator: Comparator.NOT_MATCH_PHRASE },
|
||||||
];
|
];
|
||||||
|
|
||||||
const baseAlertParams: Pick<LogDocumentCountAlertParams, 'count' | 'timeSize' | 'timeUnit'> = {
|
const baseAlertParams: Pick<AlertParams, 'count' | 'timeSize' | 'timeUnit'> = {
|
||||||
count: {
|
count: {
|
||||||
comparator: Comparator.GT,
|
comparator: Comparator.GT,
|
||||||
value: 5,
|
value: 5,
|
||||||
|
@ -85,7 +85,7 @@ describe('Log threshold executor', () => {
|
||||||
});
|
});
|
||||||
describe('Criteria filter building', () => {
|
describe('Criteria filter building', () => {
|
||||||
test('Handles positive criteria', () => {
|
test('Handles positive criteria', () => {
|
||||||
const alertParams: LogDocumentCountAlertParams = {
|
const alertParams: AlertParams = {
|
||||||
...baseAlertParams,
|
...baseAlertParams,
|
||||||
criteria: positiveCriteria,
|
criteria: positiveCriteria,
|
||||||
};
|
};
|
||||||
|
@ -140,7 +140,7 @@ describe('Log threshold executor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Handles negative criteria', () => {
|
test('Handles negative criteria', () => {
|
||||||
const alertParams: LogDocumentCountAlertParams = {
|
const alertParams: AlertParams = {
|
||||||
...baseAlertParams,
|
...baseAlertParams,
|
||||||
criteria: negativeCriteria,
|
criteria: negativeCriteria,
|
||||||
};
|
};
|
||||||
|
@ -168,7 +168,7 @@ describe('Log threshold executor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Handles time range', () => {
|
test('Handles time range', () => {
|
||||||
const alertParams: LogDocumentCountAlertParams = { ...baseAlertParams, criteria: [] };
|
const alertParams: AlertParams = { ...baseAlertParams, criteria: [] };
|
||||||
const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD);
|
const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD);
|
||||||
expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number');
|
expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number');
|
||||||
expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number');
|
expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number');
|
||||||
|
@ -183,7 +183,7 @@ describe('Log threshold executor', () => {
|
||||||
describe('ES queries', () => {
|
describe('ES queries', () => {
|
||||||
describe('Query generation', () => {
|
describe('Query generation', () => {
|
||||||
test('Correctly generates ungrouped queries', () => {
|
test('Correctly generates ungrouped queries', () => {
|
||||||
const alertParams: LogDocumentCountAlertParams = {
|
const alertParams: AlertParams = {
|
||||||
...baseAlertParams,
|
...baseAlertParams,
|
||||||
criteria: [...positiveCriteria, ...negativeCriteria],
|
criteria: [...positiveCriteria, ...negativeCriteria],
|
||||||
};
|
};
|
||||||
|
@ -279,7 +279,7 @@ describe('Log threshold executor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Correctly generates grouped queries', () => {
|
test('Correctly generates grouped queries', () => {
|
||||||
const alertParams: LogDocumentCountAlertParams = {
|
const alertParams: AlertParams = {
|
||||||
...baseAlertParams,
|
...baseAlertParams,
|
||||||
groupBy: ['host.name'],
|
groupBy: ['host.name'],
|
||||||
criteria: [...positiveCriteria, ...negativeCriteria],
|
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: {
|
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',
|
conditions: ' numericField more than 10',
|
||||||
group: null,
|
group: null,
|
||||||
matchingDocuments: 10,
|
matchingDocuments: 10,
|
||||||
|
isRatio: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -593,6 +594,7 @@ describe('Log threshold executor', () => {
|
||||||
conditions: ' numericField more than 10',
|
conditions: ' numericField more than 10',
|
||||||
group: 'i-am-a-host-name-1, i-am-a-dataset-1',
|
group: 'i-am-a-host-name-1, i-am-a-dataset-1',
|
||||||
matchingDocuments: 10,
|
matchingDocuments: 10,
|
||||||
|
isRatio: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -612,6 +614,7 @@ describe('Log threshold executor', () => {
|
||||||
conditions: ' numericField more than 10',
|
conditions: ' numericField more than 10',
|
||||||
group: 'i-am-a-host-name-3, i-am-a-dataset-3',
|
group: 'i-am-a-host-name-3, i-am-a-dataset-3',
|
||||||
matchingDocuments: 20,
|
matchingDocuments: 20,
|
||||||
|
isRatio: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -14,14 +14,21 @@ import {
|
||||||
import {
|
import {
|
||||||
AlertStates,
|
AlertStates,
|
||||||
Comparator,
|
Comparator,
|
||||||
LogDocumentCountAlertParams,
|
AlertParams,
|
||||||
Criterion,
|
Criterion,
|
||||||
GroupedSearchQueryResponseRT,
|
GroupedSearchQueryResponseRT,
|
||||||
UngroupedSearchQueryResponseRT,
|
UngroupedSearchQueryResponseRT,
|
||||||
UngroupedSearchQueryResponse,
|
UngroupedSearchQueryResponse,
|
||||||
GroupedSearchQueryResponse,
|
GroupedSearchQueryResponse,
|
||||||
LogDocumentCountAlertParamsRT,
|
AlertParamsRT,
|
||||||
} from '../../../../common/alerting/logs/types';
|
isRatioAlertParams,
|
||||||
|
hasGroupBy,
|
||||||
|
getNumerator,
|
||||||
|
getDenominator,
|
||||||
|
Criteria,
|
||||||
|
CountAlertParams,
|
||||||
|
RatioAlertParams,
|
||||||
|
} from '../../../../common/alerting/logs/log_threshold/types';
|
||||||
import { InfraBackendLibs } from '../../infra_types';
|
import { InfraBackendLibs } from '../../infra_types';
|
||||||
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
|
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
|
||||||
import { decodeOrThrow } from '../../../../common/runtime_types';
|
import { decodeOrThrow } from '../../../../common/runtime_types';
|
||||||
|
@ -42,7 +49,6 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
|
||||||
async function ({ services, params }: AlertExecutorOptions) {
|
async function ({ services, params }: AlertExecutorOptions) {
|
||||||
const { alertInstanceFactory, savedObjectsClient, callCluster } = services;
|
const { alertInstanceFactory, savedObjectsClient, callCluster } = services;
|
||||||
const { sources } = libs;
|
const { sources } = libs;
|
||||||
const { groupBy } = params;
|
|
||||||
|
|
||||||
const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default');
|
const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default');
|
||||||
const indexPattern = sourceConfiguration.configuration.logAlias;
|
const indexPattern = sourceConfiguration.configuration.logAlias;
|
||||||
|
@ -50,30 +56,23 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
|
||||||
const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY);
|
const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const validatedParams = decodeOrThrow(LogDocumentCountAlertParamsRT)(params);
|
const validatedParams = decodeOrThrow(AlertParamsRT)(params);
|
||||||
|
|
||||||
const query =
|
if (!isRatioAlertParams(validatedParams)) {
|
||||||
groupBy && groupBy.length > 0
|
await executeAlert(
|
||||||
? 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),
|
|
||||||
validatedParams,
|
validatedParams,
|
||||||
alertInstanceFactory,
|
timestampField,
|
||||||
updateAlertInstance
|
indexPattern,
|
||||||
|
callCluster,
|
||||||
|
alertInstanceFactory
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
processUngroupedResults(
|
await executeRatioAlert(
|
||||||
await getUngroupedResults(query, callCluster),
|
|
||||||
validatedParams,
|
validatedParams,
|
||||||
alertInstanceFactory,
|
timestampField,
|
||||||
updateAlertInstance
|
indexPattern,
|
||||||
|
callCluster,
|
||||||
|
alertInstanceFactory
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 = (
|
export const processUngroupedResults = (
|
||||||
results: UngroupedSearchQueryResponse,
|
results: UngroupedSearchQueryResponse,
|
||||||
params: LogDocumentCountAlertParams,
|
params: CountAlertParams,
|
||||||
alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'],
|
alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'],
|
||||||
alertInstaceUpdater: AlertInstanceUpdater
|
alertInstaceUpdater: AlertInstanceUpdater
|
||||||
) => {
|
) => {
|
||||||
|
@ -102,8 +189,9 @@ export const processUngroupedResults = (
|
||||||
actionGroup: FIRED_ACTIONS.id,
|
actionGroup: FIRED_ACTIONS.id,
|
||||||
context: {
|
context: {
|
||||||
matchingDocuments: documentCount,
|
matchingDocuments: documentCount,
|
||||||
conditions: createConditionsMessage(criteria),
|
conditions: createConditionsMessageForCriteria(criteria),
|
||||||
group: null,
|
group: null,
|
||||||
|
isRatio: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -112,24 +200,71 @@ export const processUngroupedResults = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ReducedGroupByResults {
|
export const processUngroupedRatioResults = (
|
||||||
name: string;
|
numeratorResults: UngroupedSearchQueryResponse,
|
||||||
documentCount: number;
|
denominatorResults: UngroupedSearchQueryResponse,
|
||||||
}
|
params: RatioAlertParams,
|
||||||
|
|
||||||
export const processGroupByResults = (
|
|
||||||
results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'],
|
|
||||||
params: LogDocumentCountAlertParams,
|
|
||||||
alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'],
|
alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'],
|
||||||
alertInstaceUpdater: AlertInstanceUpdater
|
alertInstaceUpdater: AlertInstanceUpdater
|
||||||
) => {
|
) => {
|
||||||
const { count, criteria } = params;
|
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 groupName = Object.values(groupBucket.key).join(', ');
|
||||||
const groupResult = { name: groupName, documentCount: groupBucket.filtered_results.doc_count };
|
const groupResult = { name: groupName, documentCount: groupBucket.filtered_results.doc_count };
|
||||||
return [...acc, groupResult];
|
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) => {
|
groupResults.forEach((group) => {
|
||||||
const alertInstance = alertInstanceFactory(group.name);
|
const alertInstance = alertInstanceFactory(group.name);
|
||||||
|
@ -141,8 +276,53 @@ export const processGroupByResults = (
|
||||||
actionGroup: FIRED_ACTIONS.id,
|
actionGroup: FIRED_ACTIONS.id,
|
||||||
context: {
|
context: {
|
||||||
matchingDocuments: documentCount,
|
matchingDocuments: documentCount,
|
||||||
conditions: createConditionsMessage(criteria),
|
conditions: createConditionsMessageForCriteria(criteria),
|
||||||
group: group.name,
|
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 = (
|
export const buildFiltersFromCriteria = (
|
||||||
params: Omit<LogDocumentCountAlertParams, 'count'>,
|
params: Pick<AlertParams, 'timeSize' | 'timeUnit'> & { criteria: Criteria },
|
||||||
timestampField: string
|
timestampField: string
|
||||||
) => {
|
) => {
|
||||||
const { timeSize, timeUnit, criteria } = params;
|
const { timeSize, timeUnit, criteria } = params;
|
||||||
|
@ -223,7 +403,7 @@ export const buildFiltersFromCriteria = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGroupedESQuery = (
|
export const getGroupedESQuery = (
|
||||||
params: Omit<LogDocumentCountAlertParams, 'count'>,
|
params: Pick<AlertParams, 'timeSize' | 'timeUnit' | 'groupBy'> & { criteria: Criteria },
|
||||||
timestampField: string,
|
timestampField: string,
|
||||||
index: string
|
index: string
|
||||||
): object | undefined => {
|
): object | undefined => {
|
||||||
|
@ -254,6 +434,7 @@ export const getGroupedESQuery = (
|
||||||
bool: {
|
bool: {
|
||||||
// Scope the inner filtering back to the unpadded range
|
// Scope the inner filtering back to the unpadded range
|
||||||
filter: [rangeFilter, ...mustFilters],
|
filter: [rangeFilter, ...mustFilters],
|
||||||
|
...(mustNotFilters.length > 0 && { must_not: mustNotFilters }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -265,7 +446,6 @@ export const getGroupedESQuery = (
|
||||||
query: {
|
query: {
|
||||||
bool: {
|
bool: {
|
||||||
filter: [groupedRangeFilter],
|
filter: [groupedRangeFilter],
|
||||||
...(mustNotFilters.length > 0 && { must_not: mustNotFilters }),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
aggregations,
|
aggregations,
|
||||||
|
@ -281,7 +461,7 @@ export const getGroupedESQuery = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUngroupedESQuery = (
|
export const getUngroupedESQuery = (
|
||||||
params: Omit<LogDocumentCountAlertParams, 'count'>,
|
params: Pick<AlertParams, 'timeSize' | 'timeUnit'> & { criteria: Criteria },
|
||||||
timestampField: string,
|
timestampField: string,
|
||||||
index: string
|
index: string
|
||||||
): object => {
|
): object => {
|
||||||
|
@ -315,7 +495,7 @@ type Filter = {
|
||||||
[key in SupportedESQueryTypes]?: object;
|
[key in SupportedESQueryTypes]?: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildFiltersForCriteria = (criteria: LogDocumentCountAlertParams['criteria']) => {
|
const buildFiltersForCriteria = (criteria: Criteria) => {
|
||||||
let filters: Filter[] = [];
|
let filters: Filter[] = [];
|
||||||
|
|
||||||
criteria.forEach((criterion) => {
|
criteria.forEach((criterion) => {
|
||||||
|
@ -443,7 +623,7 @@ const getGroupedResults = async (query: object, callCluster: AlertServices['call
|
||||||
return compositeGroupBuckets;
|
return compositeGroupBuckets;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createConditionsMessage = (criteria: LogDocumentCountAlertParams['criteria']) => {
|
const createConditionsMessageForCriteria = (criteria: Criteria) => {
|
||||||
const parts = criteria.map((criterion, index) => {
|
const parts = criteria.map((criterion, index) => {
|
||||||
const { field, comparator, value } = criterion;
|
const { field, comparator, value } = criterion;
|
||||||
return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`;
|
return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`;
|
||||||
|
|
|
@ -4,14 +4,14 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { schema } from '@kbn/config-schema';
|
|
||||||
import { PluginSetupContract } from '../../../../../alerts/server';
|
import { PluginSetupContract } from '../../../../../alerts/server';
|
||||||
import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor';
|
import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor';
|
||||||
import {
|
import {
|
||||||
LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
|
LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
|
||||||
Comparator,
|
AlertParamsRT,
|
||||||
} from '../../../../common/alerting/logs/types';
|
} from '../../../../common/alerting/logs/log_threshold/types';
|
||||||
import { InfraBackendLibs } from '../../infra_types';
|
import { InfraBackendLibs } from '../../infra_types';
|
||||||
|
import { decodeOrThrow } from '../../../../common/runtime_types';
|
||||||
|
|
||||||
const documentCountActionVariableDescription = i18n.translate(
|
const documentCountActionVariableDescription = i18n.translate(
|
||||||
'xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription',
|
'xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription',
|
||||||
|
@ -34,33 +34,33 @@ const groupByActionVariableDescription = i18n.translate(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const countSchema = schema.object({
|
const isRatioActionVariableDescription = i18n.translate(
|
||||||
value: schema.number(),
|
'xpack.infra.logs.alerting.threshold.isRatioActionVariableDescription',
|
||||||
comparator: schema.oneOf([
|
{
|
||||||
schema.literal(Comparator.GT),
|
defaultMessage: 'Denotes whether this alert was configured with a ratio',
|
||||||
schema.literal(Comparator.LT),
|
}
|
||||||
schema.literal(Comparator.GT_OR_EQ),
|
);
|
||||||
schema.literal(Comparator.LT_OR_EQ),
|
|
||||||
schema.literal(Comparator.EQ),
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
|
|
||||||
const criteriaSchema = schema.object({
|
const ratioActionVariableDescription = i18n.translate(
|
||||||
field: schema.string(),
|
'xpack.infra.logs.alerting.threshold.ratioActionVariableDescription',
|
||||||
comparator: schema.oneOf([
|
{
|
||||||
schema.literal(Comparator.GT),
|
defaultMessage: 'The ratio value of the two sets of criteria',
|
||||||
schema.literal(Comparator.LT),
|
}
|
||||||
schema.literal(Comparator.GT_OR_EQ),
|
);
|
||||||
schema.literal(Comparator.LT_OR_EQ),
|
|
||||||
schema.literal(Comparator.EQ),
|
const numeratorConditionsActionVariableDescription = i18n.translate(
|
||||||
schema.literal(Comparator.NOT_EQ),
|
'xpack.infra.logs.alerting.threshold.numeratorConditionsActionVariableDescription',
|
||||||
schema.literal(Comparator.MATCH),
|
{
|
||||||
schema.literal(Comparator.NOT_MATCH),
|
defaultMessage: 'The conditions that the numerator of the ratio needed to fulfill',
|
||||||
schema.literal(Comparator.MATCH_PHRASE),
|
}
|
||||||
schema.literal(Comparator.NOT_MATCH_PHRASE),
|
);
|
||||||
]),
|
|
||||||
value: schema.oneOf([schema.number(), schema.string()]),
|
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(
|
export async function registerLogThresholdAlertType(
|
||||||
alertingPlugin: PluginSetupContract,
|
alertingPlugin: PluginSetupContract,
|
||||||
|
@ -76,13 +76,9 @@ export async function registerLogThresholdAlertType(
|
||||||
id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
|
id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
|
||||||
name: 'Log threshold',
|
name: 'Log threshold',
|
||||||
validate: {
|
validate: {
|
||||||
params: schema.object({
|
params: {
|
||||||
count: countSchema,
|
validate: (params) => decodeOrThrow(AlertParamsRT)(params),
|
||||||
criteria: schema.arrayOf(criteriaSchema),
|
},
|
||||||
timeUnit: schema.string(),
|
|
||||||
timeSize: schema.number(),
|
|
||||||
groupBy: schema.maybe(schema.arrayOf(schema.string())),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
defaultActionGroupId: FIRED_ACTIONS.id,
|
defaultActionGroupId: FIRED_ACTIONS.id,
|
||||||
actionGroups: [FIRED_ACTIONS],
|
actionGroups: [FIRED_ACTIONS],
|
||||||
|
@ -92,6 +88,14 @@ export async function registerLogThresholdAlertType(
|
||||||
{ name: 'matchingDocuments', description: documentCountActionVariableDescription },
|
{ name: 'matchingDocuments', description: documentCountActionVariableDescription },
|
||||||
{ name: 'conditions', description: conditionsActionVariableDescription },
|
{ name: 'conditions', description: conditionsActionVariableDescription },
|
||||||
{ name: 'group', description: groupByActionVariableDescription },
|
{ name: 'group', description: groupByActionVariableDescription },
|
||||||
|
// Ratio alerts
|
||||||
|
{ name: 'isRatio', description: isRatioActionVariableDescription },
|
||||||
|
{ name: 'ratio', description: ratioActionVariableDescription },
|
||||||
|
{ name: 'numeratorConditions', description: numeratorConditionsActionVariableDescription },
|
||||||
|
{
|
||||||
|
name: 'denominatorConditions',
|
||||||
|
description: denominatorConditionsActionVariableDescription,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
producer: 'logs',
|
producer: 'logs',
|
||||||
|
|
|
@ -8319,13 +8319,9 @@
|
||||||
"xpack.infra.logs.alertFlyout.alertName": "ログしきい値",
|
"xpack.infra.logs.alertFlyout.alertName": "ログしきい値",
|
||||||
"xpack.infra.logs.alertFlyout.criterionComparatorValueTitle": "比較:値",
|
"xpack.infra.logs.alertFlyout.criterionComparatorValueTitle": "比較:値",
|
||||||
"xpack.infra.logs.alertFlyout.criterionFieldTitle": "フィールド",
|
"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.criterionComparatorRequired": "コンパレーターが必要です。",
|
||||||
"xpack.infra.logs.alertFlyout.error.criterionFieldRequired": "フィールドが必要です。",
|
"xpack.infra.logs.alertFlyout.error.criterionFieldRequired": "フィールドが必要です。",
|
||||||
"xpack.infra.logs.alertFlyout.error.criterionValueRequired": "値が必要です。",
|
"xpack.infra.logs.alertFlyout.error.criterionValueRequired": "値が必要です。",
|
||||||
"xpack.infra.logs.alertFlyout.error.documentCountRequired": "ドキュメントカウントが必要です。",
|
|
||||||
"xpack.infra.logs.alertFlyout.error.timeSizeRequired": "ページサイズが必要です。",
|
"xpack.infra.logs.alertFlyout.error.timeSizeRequired": "ページサイズが必要です。",
|
||||||
"xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix": "With",
|
"xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix": "With",
|
||||||
"xpack.infra.logs.alertFlyout.removeCondition": "条件を削除",
|
"xpack.infra.logs.alertFlyout.removeCondition": "条件を削除",
|
||||||
|
|
|
@ -8323,13 +8323,9 @@
|
||||||
"xpack.infra.logs.alertFlyout.alertName": "日志阈值",
|
"xpack.infra.logs.alertFlyout.alertName": "日志阈值",
|
||||||
"xpack.infra.logs.alertFlyout.criterionComparatorValueTitle": "对比:值",
|
"xpack.infra.logs.alertFlyout.criterionComparatorValueTitle": "对比:值",
|
||||||
"xpack.infra.logs.alertFlyout.criterionFieldTitle": "字段",
|
"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.criterionComparatorRequired": "比较运算符必填。",
|
||||||
"xpack.infra.logs.alertFlyout.error.criterionFieldRequired": "“字段”必填。",
|
"xpack.infra.logs.alertFlyout.error.criterionFieldRequired": "“字段”必填。",
|
||||||
"xpack.infra.logs.alertFlyout.error.criterionValueRequired": "“值”必填。",
|
"xpack.infra.logs.alertFlyout.error.criterionValueRequired": "“值”必填。",
|
||||||
"xpack.infra.logs.alertFlyout.error.documentCountRequired": "“文档计数”必填。",
|
|
||||||
"xpack.infra.logs.alertFlyout.error.timeSizeRequired": "“时间大小”必填。",
|
"xpack.infra.logs.alertFlyout.error.timeSizeRequired": "“时间大小”必填。",
|
||||||
"xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix": "具有",
|
"xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix": "具有",
|
||||||
"xpack.infra.logs.alertFlyout.removeCondition": "删除条件",
|
"xpack.infra.logs.alertFlyout.removeCondition": "删除条件",
|
||||||
|
|
Loading…
Reference in a new issue