[Logs UI] Fix initial selection of log threshold alert condition field if missing from mapping (#86488)
This avoid selecting the `log.level` field as the default in log threshold alerts if it is not present in the mapping of at least one source index.
This commit is contained in:
parent
a98550b4f1
commit
24db3a0070
|
@ -105,29 +105,38 @@ const ThresholdRT = rt.type({
|
|||
|
||||
export type Threshold = rt.TypeOf<typeof ThresholdRT>;
|
||||
|
||||
export const CriterionRT = rt.type({
|
||||
export const criterionRT = rt.type({
|
||||
field: rt.string,
|
||||
comparator: ComparatorRT,
|
||||
value: rt.union([rt.string, rt.number]),
|
||||
});
|
||||
export type Criterion = rt.TypeOf<typeof criterionRT>;
|
||||
|
||||
export type Criterion = rt.TypeOf<typeof CriterionRT>;
|
||||
export const criteriaRT = rt.array(CriterionRT);
|
||||
export type Criteria = rt.TypeOf<typeof criteriaRT>;
|
||||
export const partialCriterionRT = rt.partial(criterionRT.props);
|
||||
export type PartialCriterion = rt.TypeOf<typeof partialCriterionRT>;
|
||||
|
||||
export const countCriteriaRT = criteriaRT;
|
||||
export const countCriteriaRT = rt.array(criterionRT);
|
||||
export type CountCriteria = rt.TypeOf<typeof countCriteriaRT>;
|
||||
|
||||
export const ratioCriteriaRT = rt.tuple([criteriaRT, criteriaRT]);
|
||||
export const partialCountCriteriaRT = rt.array(partialCriterionRT);
|
||||
export type PartialCountCriteria = rt.TypeOf<typeof partialCountCriteriaRT>;
|
||||
|
||||
export const ratioCriteriaRT = rt.tuple([countCriteriaRT, countCriteriaRT]);
|
||||
export type RatioCriteria = rt.TypeOf<typeof ratioCriteriaRT>;
|
||||
|
||||
export const TimeUnitRT = rt.union([
|
||||
export const partialRatioCriteriaRT = rt.tuple([partialCountCriteriaRT, partialCountCriteriaRT]);
|
||||
export type PartialRatioCriteria = rt.TypeOf<typeof partialRatioCriteriaRT>;
|
||||
|
||||
export const partialCriteriaRT = rt.union([partialCountCriteriaRT, partialRatioCriteriaRT]);
|
||||
export type PartialCriteria = rt.TypeOf<typeof partialCriteriaRT>;
|
||||
|
||||
export const timeUnitRT = rt.union([
|
||||
rt.literal('s'),
|
||||
rt.literal('m'),
|
||||
rt.literal('h'),
|
||||
rt.literal('d'),
|
||||
]);
|
||||
export type TimeUnit = rt.TypeOf<typeof TimeUnitRT>;
|
||||
export type TimeUnit = rt.TypeOf<typeof timeUnitRT>;
|
||||
|
||||
export const timeSizeRT = rt.number;
|
||||
export const groupByRT = rt.array(rt.string);
|
||||
|
@ -136,15 +145,18 @@ 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,
|
||||
timeUnit: timeUnitRT,
|
||||
timeSize: timeSizeRT,
|
||||
});
|
||||
|
||||
const partialRequiredAlertParamsRT = rt.partial(RequiredAlertParamsRT.props);
|
||||
export type PartialRequiredAlertParams = rt.TypeOf<typeof partialRequiredAlertParamsRT>;
|
||||
|
||||
const OptionalAlertParamsRT = rt.partial({
|
||||
groupBy: groupByRT,
|
||||
});
|
||||
|
||||
export const alertParamsRT = rt.intersection([
|
||||
export const countAlertParamsRT = rt.intersection([
|
||||
rt.type({
|
||||
criteria: countCriteriaRT,
|
||||
...RequiredAlertParamsRT.props,
|
||||
|
@ -153,8 +165,18 @@ export const alertParamsRT = rt.intersection([
|
|||
...OptionalAlertParamsRT.props,
|
||||
}),
|
||||
]);
|
||||
export type CountAlertParams = rt.TypeOf<typeof countAlertParamsRT>;
|
||||
|
||||
export type CountAlertParams = rt.TypeOf<typeof alertParamsRT>;
|
||||
export const partialCountAlertParamsRT = rt.intersection([
|
||||
rt.type({
|
||||
criteria: partialCountCriteriaRT,
|
||||
...RequiredAlertParamsRT.props,
|
||||
}),
|
||||
rt.partial({
|
||||
...OptionalAlertParamsRT.props,
|
||||
}),
|
||||
]);
|
||||
export type PartialCountAlertParams = rt.TypeOf<typeof partialCountAlertParamsRT>;
|
||||
|
||||
export const ratioAlertParamsRT = rt.intersection([
|
||||
rt.type({
|
||||
|
@ -165,13 +187,29 @@ export const ratioAlertParamsRT = rt.intersection([
|
|||
...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 partialRatioAlertParamsRT = rt.intersection([
|
||||
rt.type({
|
||||
criteria: partialRatioCriteriaRT,
|
||||
...RequiredAlertParamsRT.props,
|
||||
}),
|
||||
rt.partial({
|
||||
...OptionalAlertParamsRT.props,
|
||||
}),
|
||||
]);
|
||||
export type PartialRatioAlertParams = rt.TypeOf<typeof partialRatioAlertParamsRT>;
|
||||
|
||||
export const isRatioAlert = (criteria: AlertParams['criteria']): criteria is RatioCriteria => {
|
||||
export const alertParamsRT = rt.union([countAlertParamsRT, ratioAlertParamsRT]);
|
||||
export type AlertParams = rt.TypeOf<typeof alertParamsRT>;
|
||||
|
||||
export const partialAlertParamsRT = rt.union([
|
||||
partialCountAlertParamsRT,
|
||||
partialRatioAlertParamsRT,
|
||||
]);
|
||||
export type PartialAlertParams = rt.TypeOf<typeof partialAlertParamsRT>;
|
||||
|
||||
export const isRatioAlert = (criteria: PartialCriteria): criteria is PartialRatioCriteria => {
|
||||
return criteria.length > 0 && Array.isArray(criteria[0]) ? true : false;
|
||||
};
|
||||
|
||||
|
@ -179,11 +217,13 @@ export const isRatioAlertParams = (params: AlertParams): params is RatioAlertPar
|
|||
return isRatioAlert(params.criteria);
|
||||
};
|
||||
|
||||
export const getNumerator = (criteria: RatioCriteria): Criteria => {
|
||||
export const getNumerator = <C extends RatioCriteria | PartialRatioCriteria>(criteria: C): C[0] => {
|
||||
return criteria[0];
|
||||
};
|
||||
|
||||
export const getDenominator = (criteria: RatioCriteria): Criteria => {
|
||||
export const getDenominator = <C extends RatioCriteria | PartialRatioCriteria>(
|
||||
criteria: C
|
||||
): C[1] => {
|
||||
return criteria[1];
|
||||
};
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import * as rt from 'io-ts';
|
||||
import {
|
||||
criteriaRT,
|
||||
TimeUnitRT,
|
||||
countCriteriaRT,
|
||||
timeUnitRT,
|
||||
timeSizeRT,
|
||||
groupByRT,
|
||||
} from '../../alerting/logs/log_threshold/types';
|
||||
|
@ -42,8 +42,8 @@ export type GetLogAlertsChartPreviewDataSuccessResponsePayload = rt.TypeOf<
|
|||
|
||||
export const getLogAlertsChartPreviewDataAlertParamsSubsetRT = rt.intersection([
|
||||
rt.type({
|
||||
criteria: criteriaRT,
|
||||
timeUnit: TimeUnitRT,
|
||||
criteria: countCriteriaRT,
|
||||
timeUnit: timeUnitRT,
|
||||
timeSize: timeSizeRT,
|
||||
}),
|
||||
rt.partial({
|
||||
|
|
|
@ -43,3 +43,6 @@ export type DeepPartial<T> = T extends any[]
|
|||
interface DeepPartialArray<T> extends Array<DeepPartial<T>> {}
|
||||
|
||||
type DeepPartialObject<T> = { [P in keyof T]+?: DeepPartial<T[P]> };
|
||||
|
||||
export type ObjectEntry<T> = [keyof T, T[keyof T]];
|
||||
export type ObjectEntries<T> = Array<ObjectEntry<T>>;
|
||||
|
|
|
@ -11,12 +11,11 @@ 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,
|
||||
PartialAlertParams,
|
||||
PartialCountCriteria as PartialCountCriteriaType,
|
||||
PartialCriteria as PartialCriteriaType,
|
||||
PartialCriterion as PartialCriterionType,
|
||||
PartialRatioCriteria as PartialRatioCriteriaType,
|
||||
isRatioAlert,
|
||||
getNumerator,
|
||||
getDenominator,
|
||||
|
@ -25,8 +24,6 @@ import { Errors, CriterionErrors } from '../../validation';
|
|||
import { ExpressionLike } from './editor';
|
||||
import { CriterionPreview } from './criterion_preview_chart';
|
||||
|
||||
const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' };
|
||||
|
||||
const QueryAText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCriteriaQueryAText', {
|
||||
defaultMessage: 'Query A',
|
||||
});
|
||||
|
@ -37,11 +34,12 @@ const QueryBText = i18n.translate('xpack.infra.logs.alerting.threshold.ratioCrit
|
|||
|
||||
interface SharedProps {
|
||||
fields: IFieldType[];
|
||||
criteria?: AlertParams['criteria'];
|
||||
criteria?: PartialCriteriaType;
|
||||
defaultCriterion: PartialCriterionType;
|
||||
errors: Errors['criteria'];
|
||||
alertParams: Partial<AlertParams>;
|
||||
alertParams: PartialAlertParams;
|
||||
sourceId: string;
|
||||
updateCriteria: (criteria: AlertParams['criteria']) => void;
|
||||
updateCriteria: (criteria: PartialCriteriaType) => void;
|
||||
}
|
||||
|
||||
type CriteriaProps = SharedProps;
|
||||
|
@ -60,10 +58,10 @@ export const Criteria: React.FC<CriteriaProps> = (props) => {
|
|||
interface CriteriaWrapperProps {
|
||||
alertParams: SharedProps['alertParams'];
|
||||
fields: SharedProps['fields'];
|
||||
updateCriterion: (idx: number, params: Partial<CriterionType>) => void;
|
||||
updateCriterion: (idx: number, params: PartialCriterionType) => void;
|
||||
removeCriterion: (idx: number) => void;
|
||||
addCriterion: () => void;
|
||||
criteria: CriteriaType;
|
||||
criteria: PartialCountCriteriaType;
|
||||
errors: CriterionErrors;
|
||||
sourceId: SharedProps['sourceId'];
|
||||
isRatio?: boolean;
|
||||
|
@ -118,29 +116,24 @@ const CriteriaWrapper: React.FC<CriteriaWrapperProps> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface RatioCriteriaProps {
|
||||
alertParams: SharedProps['alertParams'];
|
||||
fields: SharedProps['fields'];
|
||||
criteria: RatioCriteriaType;
|
||||
errors: Errors['criteria'];
|
||||
sourceId: SharedProps['sourceId'];
|
||||
updateCriteria: (criteria: AlertParams['criteria']) => void;
|
||||
interface RatioCriteriaProps extends SharedProps {
|
||||
criteria: PartialRatioCriteriaType;
|
||||
}
|
||||
|
||||
const RatioCriteria: React.FC<RatioCriteriaProps> = (props) => {
|
||||
const { criteria, errors, updateCriteria } = props;
|
||||
const { criteria, defaultCriterion, errors, updateCriteria } = props;
|
||||
|
||||
const handleUpdateNumeratorCriteria = useCallback(
|
||||
(criteriaParam: CriteriaType) => {
|
||||
const nextCriteria: RatioCriteriaType = [criteriaParam, getDenominator(criteria)];
|
||||
(criteriaParam: PartialCountCriteriaType) => {
|
||||
const nextCriteria: PartialRatioCriteriaType = [criteriaParam, getDenominator(criteria)];
|
||||
updateCriteria(nextCriteria);
|
||||
},
|
||||
[updateCriteria, criteria]
|
||||
);
|
||||
|
||||
const handleUpdateDenominatorCriteria = useCallback(
|
||||
(criteriaParam: CriteriaType) => {
|
||||
const nextCriteria: RatioCriteriaType = [getNumerator(criteria), criteriaParam];
|
||||
(criteriaParam: PartialCountCriteriaType) => {
|
||||
const nextCriteria: PartialRatioCriteriaType = [getNumerator(criteria), criteriaParam];
|
||||
updateCriteria(nextCriteria);
|
||||
},
|
||||
[updateCriteria, criteria]
|
||||
|
@ -150,13 +143,13 @@ const RatioCriteria: React.FC<RatioCriteriaProps> = (props) => {
|
|||
updateCriterion: updateNumeratorCriterion,
|
||||
addCriterion: addNumeratorCriterion,
|
||||
removeCriterion: removeNumeratorCriterion,
|
||||
} = useCriteriaState(getNumerator(criteria), handleUpdateNumeratorCriteria);
|
||||
} = useCriteriaState(getNumerator(criteria), defaultCriterion, handleUpdateNumeratorCriteria);
|
||||
|
||||
const {
|
||||
updateCriterion: updateDenominatorCriterion,
|
||||
addCriterion: addDenominatorCriterion,
|
||||
removeCriterion: removeDenominatorCriterion,
|
||||
} = useCriteriaState(getDenominator(criteria), handleUpdateDenominatorCriteria);
|
||||
} = useCriteriaState(getDenominator(criteria), defaultCriterion, handleUpdateDenominatorCriteria);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -191,28 +184,17 @@ const RatioCriteria: React.FC<RatioCriteriaProps> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface CountCriteriaProps {
|
||||
alertParams: SharedProps['alertParams'];
|
||||
fields: SharedProps['fields'];
|
||||
criteria: CountCriteriaType;
|
||||
errors: Errors['criteria'];
|
||||
sourceId: SharedProps['sourceId'];
|
||||
updateCriteria: (criteria: AlertParams['criteria']) => void;
|
||||
interface CountCriteriaProps extends SharedProps {
|
||||
criteria: PartialCountCriteriaType;
|
||||
}
|
||||
|
||||
const CountCriteria: React.FC<CountCriteriaProps> = (props) => {
|
||||
const { criteria, updateCriteria, errors } = props;
|
||||
|
||||
const handleUpdateCriteria = useCallback(
|
||||
(criteriaParam: CriteriaType) => {
|
||||
updateCriteria(criteriaParam);
|
||||
},
|
||||
[updateCriteria]
|
||||
);
|
||||
const { criteria, defaultCriterion, updateCriteria, errors } = props;
|
||||
|
||||
const { updateCriterion, addCriterion, removeCriterion } = useCriteriaState(
|
||||
criteria,
|
||||
handleUpdateCriteria
|
||||
defaultCriterion,
|
||||
updateCriteria
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -227,8 +209,9 @@ const CountCriteria: React.FC<CountCriteriaProps> = (props) => {
|
|||
};
|
||||
|
||||
const useCriteriaState = (
|
||||
criteria: CriteriaType,
|
||||
onUpdateCriteria: (criteria: CriteriaType) => void
|
||||
criteria: PartialCountCriteriaType,
|
||||
defaultCriterion: PartialCriterionType,
|
||||
onUpdateCriteria: (criteria: PartialCountCriteriaType) => void
|
||||
) => {
|
||||
const updateCriterion = useCallback(
|
||||
(idx, criterionParams) => {
|
||||
|
@ -241,13 +224,13 @@ const useCriteriaState = (
|
|||
);
|
||||
|
||||
const addCriterion = useCallback(() => {
|
||||
const nextCriteria = criteria ? [...criteria, DEFAULT_CRITERIA] : [DEFAULT_CRITERIA];
|
||||
const nextCriteria = [...criteria, defaultCriterion];
|
||||
onUpdateCriteria(nextCriteria);
|
||||
}, [criteria, onUpdateCriteria]);
|
||||
}, [criteria, defaultCriterion, onUpdateCriteria]);
|
||||
|
||||
const removeCriterion = useCallback(
|
||||
(idx) => {
|
||||
const nextCriteria = criteria.filter((criterion, index) => {
|
||||
const nextCriteria = criteria.filter((_criterion, index) => {
|
||||
return index !== idx;
|
||||
});
|
||||
onUpdateCriteria(nextCriteria);
|
||||
|
|
|
@ -90,7 +90,7 @@ const getFieldInfo = (fields: IFieldType[], fieldName: string): IFieldType | und
|
|||
interface Props {
|
||||
idx: number;
|
||||
fields: IFieldType[];
|
||||
criterion: CriterionType;
|
||||
criterion: Partial<CriterionType>;
|
||||
updateCriterion: (idx: number, params: Partial<CriterionType>) => void;
|
||||
removeCriterion: (idx: number) => void;
|
||||
canDelete: boolean;
|
||||
|
@ -116,7 +116,11 @@ export const Criterion: React.FC<Props> = ({
|
|||
}, [fields]);
|
||||
|
||||
const fieldInfo: IFieldType | undefined = useMemo(() => {
|
||||
return getFieldInfo(fields, criterion.field);
|
||||
if (criterion.field) {
|
||||
return getFieldInfo(fields, criterion.field);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [fields, criterion]);
|
||||
|
||||
const compatibleComparatorOptions = useMemo(() => {
|
||||
|
@ -129,10 +133,8 @@ export const Criterion: React.FC<Props> = ({
|
|||
const nextFieldInfo = getFieldInfo(fields, fieldName);
|
||||
// If the field information we're dealing with has changed, reset the comparator and value.
|
||||
if (
|
||||
fieldInfo &&
|
||||
nextFieldInfo &&
|
||||
(fieldInfo.type !== nextFieldInfo.type ||
|
||||
fieldInfo.aggregatable !== nextFieldInfo.aggregatable)
|
||||
fieldInfo?.type !== nextFieldInfo?.type ||
|
||||
fieldInfo?.aggregatable !== nextFieldInfo?.aggregatable
|
||||
) {
|
||||
const compatibleComparators = getCompatibleComparatorsForField(nextFieldInfo);
|
||||
updateCriterion(idx, {
|
||||
|
@ -160,7 +162,7 @@ export const Criterion: React.FC<Props> = ({
|
|||
idx === 0 ? firstCriterionFieldPrefix : successiveCriterionFieldPrefix
|
||||
}
|
||||
uppercase={true}
|
||||
value={criterion.field}
|
||||
value={criterion.field ?? 'a chosen field'}
|
||||
isActive={isFieldPopoverOpen}
|
||||
color={errors.field.length === 0 ? 'secondary' : 'danger'}
|
||||
onClick={(e) => {
|
||||
|
@ -180,7 +182,8 @@ export const Criterion: React.FC<Props> = ({
|
|||
<EuiFormRow isInvalid={errors.field.length > 0} error={errors.field}>
|
||||
<EuiSelect
|
||||
compressed
|
||||
value={criterion.field}
|
||||
hasNoInitialSelection={criterion.field == null}
|
||||
value={criterion.field ?? ''}
|
||||
onChange={handleFieldChange}
|
||||
options={fieldOptions}
|
||||
/>
|
||||
|
@ -194,9 +197,11 @@ export const Criterion: React.FC<Props> = ({
|
|||
button={
|
||||
<EuiExpression
|
||||
description={
|
||||
ComparatorToi18nMap[`${criterion.comparator}:${fieldInfo?.type}`]
|
||||
? ComparatorToi18nMap[`${criterion.comparator}:${fieldInfo?.type}`]
|
||||
: ComparatorToi18nMap[criterion.comparator]
|
||||
criterion.comparator
|
||||
? ComparatorToi18nMap[`${criterion.comparator}:${fieldInfo?.type}`] ??
|
||||
ComparatorToi18nMap[criterion.comparator] ??
|
||||
''
|
||||
: ''
|
||||
}
|
||||
uppercase={true}
|
||||
value={criterion.value}
|
||||
|
@ -225,6 +230,7 @@ export const Criterion: React.FC<Props> = ({
|
|||
<EuiFormRow isInvalid={errors.comparator.length > 0} error={errors.comparator}>
|
||||
<EuiSelect
|
||||
compressed
|
||||
hasNoInitialSelection={criterion.comparator == null}
|
||||
value={criterion.comparator}
|
||||
onChange={(e) =>
|
||||
updateCriterion(idx, { comparator: e.target.value as Comparator })
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
NUM_BUCKETS,
|
||||
} from '../../../common/criterion_preview_chart/criterion_preview_chart';
|
||||
import {
|
||||
AlertParams,
|
||||
PartialAlertParams,
|
||||
Threshold,
|
||||
Criterion,
|
||||
Comparator,
|
||||
|
@ -50,7 +50,7 @@ import { decodeOrThrow } from '../../../../../common/runtime_types';
|
|||
const GROUP_LIMIT = 5;
|
||||
|
||||
interface Props {
|
||||
alertParams: Partial<AlertParams>;
|
||||
alertParams: PartialAlertParams;
|
||||
chartCriterion: Partial<Criterion>;
|
||||
sourceId: string;
|
||||
showThreshold: boolean;
|
||||
|
|
|
@ -4,25 +4,36 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLoadingSpinner, EuiSpacer, EuiButton, EuiCallOut } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import useMount from 'react-use/lib/useMount';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression';
|
||||
import { ForLastExpression } from '../../../../../../triggers_actions_ui/public';
|
||||
import {
|
||||
AlertParams,
|
||||
AlertTypeParamsExpressionProps,
|
||||
ForLastExpression,
|
||||
} from '../../../../../../triggers_actions_ui/public';
|
||||
import {
|
||||
PartialAlertParams,
|
||||
Comparator,
|
||||
ThresholdType,
|
||||
isRatioAlert,
|
||||
PartialCriteria as PartialCriteriaType,
|
||||
ThresholdType,
|
||||
timeUnitRT,
|
||||
} from '../../../../../common/alerting/logs/log_threshold/types';
|
||||
import { Threshold } from './threshold';
|
||||
import { Criteria } from './criteria';
|
||||
import { TypeSwitcher } from './type_switcher';
|
||||
import { decodeOrThrow } from '../../../../../common/runtime_types';
|
||||
import { ObjectEntries } from '../../../../../common/utility_types';
|
||||
import {
|
||||
LogIndexField,
|
||||
LogSourceProvider,
|
||||
useLogSourceContext,
|
||||
} from '../../../../containers/logs/log_source';
|
||||
import { useSourceId } from '../../../../containers/source_id';
|
||||
import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source';
|
||||
import { Errors } from '../../validation';
|
||||
import { GroupByExpression } from '../../../common/group_by_expression/group_by_expression';
|
||||
import { errorsRT } from '../../validation';
|
||||
import { Criteria } from './criteria';
|
||||
import { Threshold } from './threshold';
|
||||
import { TypeSwitcher } from './type_switcher';
|
||||
|
||||
export interface ExpressionCriteria {
|
||||
field?: string;
|
||||
|
@ -34,45 +45,46 @@ interface LogsContextMeta {
|
|||
isInternal?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
errors: Errors;
|
||||
alertParams: Partial<AlertParams>;
|
||||
setAlertParams(key: string, value: any): void;
|
||||
setAlertProperty(key: string, value: any): void;
|
||||
sourceId: string;
|
||||
metadata: LogsContextMeta;
|
||||
}
|
||||
|
||||
const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' };
|
||||
|
||||
const DEFAULT_BASE_EXPRESSION = {
|
||||
timeSize: 5,
|
||||
timeUnit: 'm',
|
||||
};
|
||||
|
||||
const DEFAULT_COUNT_EXPRESSION = {
|
||||
const DEFAULT_FIELD = 'log.level';
|
||||
|
||||
const createDefaultCriterion = (
|
||||
availableFields: LogIndexField[],
|
||||
value: ExpressionCriteria['value']
|
||||
) =>
|
||||
availableFields.some((availableField) => availableField.name === DEFAULT_FIELD)
|
||||
? { field: DEFAULT_FIELD, comparator: Comparator.EQ, value }
|
||||
: { field: undefined, comparator: undefined, value: undefined };
|
||||
|
||||
const createDefaultCountAlertParams = (availableFields: LogIndexField[]) => ({
|
||||
...DEFAULT_BASE_EXPRESSION,
|
||||
count: {
|
||||
value: 75,
|
||||
comparator: Comparator.GT,
|
||||
},
|
||||
criteria: [DEFAULT_CRITERIA],
|
||||
};
|
||||
criteria: [createDefaultCriterion(availableFields, 'error')],
|
||||
});
|
||||
|
||||
const DEFAULT_RATIO_EXPRESSION = {
|
||||
const createDefaultRatioAlertParams = (availableFields: LogIndexField[]) => ({
|
||||
...DEFAULT_BASE_EXPRESSION,
|
||||
count: {
|
||||
value: 2,
|
||||
comparator: Comparator.GT,
|
||||
},
|
||||
criteria: [
|
||||
[DEFAULT_CRITERIA],
|
||||
[{ field: 'log.level', comparator: Comparator.EQ, value: 'warning' }],
|
||||
createDefaultCriterion(availableFields, 'error'),
|
||||
createDefaultCriterion([], 'warning'),
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
export const ExpressionEditor: React.FC<Props> = (props) => {
|
||||
const isInternal = props.metadata?.isInternal;
|
||||
export const ExpressionEditor: React.FC<
|
||||
AlertTypeParamsExpressionProps<PartialAlertParams, LogsContextMeta>
|
||||
> = (props) => {
|
||||
const isInternal = props.metadata?.isInternal ?? false;
|
||||
const [sourceId] = useSourceId();
|
||||
const { http } = useKibana().services;
|
||||
|
||||
|
@ -80,12 +92,12 @@ export const ExpressionEditor: React.FC<Props> = (props) => {
|
|||
<>
|
||||
{isInternal ? (
|
||||
<SourceStatusWrapper {...props}>
|
||||
<Editor {...props} sourceId={sourceId} />
|
||||
<Editor {...props} />
|
||||
</SourceStatusWrapper>
|
||||
) : (
|
||||
<LogSourceProvider sourceId={sourceId} fetch={http!.fetch}>
|
||||
<SourceStatusWrapper {...props}>
|
||||
<Editor {...props} sourceId={sourceId} />
|
||||
<Editor {...props} />
|
||||
</SourceStatusWrapper>
|
||||
</LogSourceProvider>
|
||||
)}
|
||||
|
@ -93,7 +105,7 @@ export const ExpressionEditor: React.FC<Props> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const SourceStatusWrapper: React.FC<Props> = (props) => {
|
||||
export const SourceStatusWrapper: React.FC = ({ children }) => {
|
||||
const {
|
||||
initialize,
|
||||
isLoadingSourceStatus,
|
||||
|
@ -101,7 +113,6 @@ export const SourceStatusWrapper: React.FC<Props> = (props) => {
|
|||
hasFailedLoadingSourceStatus,
|
||||
loadSourceStatus,
|
||||
} = useLogSourceContext();
|
||||
const { children } = props;
|
||||
|
||||
useMount(() => {
|
||||
initialize();
|
||||
|
@ -136,16 +147,19 @@ export const SourceStatusWrapper: React.FC<Props> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const Editor: React.FC<Props> = (props) => {
|
||||
const { setAlertParams, alertParams, errors, sourceId } = props;
|
||||
export const Editor: React.FC<
|
||||
AlertTypeParamsExpressionProps<PartialAlertParams, LogsContextMeta>
|
||||
> = (props) => {
|
||||
const { setAlertParams, alertParams, errors } = props;
|
||||
const [hasSetDefaults, setHasSetDefaults] = useState<boolean>(false);
|
||||
const { sourceStatus } = useLogSourceContext();
|
||||
useMount(() => {
|
||||
for (const [key, value] of Object.entries({ ...DEFAULT_COUNT_EXPRESSION, ...alertParams })) {
|
||||
setAlertParams(key, value);
|
||||
}
|
||||
setHasSetDefaults(true);
|
||||
});
|
||||
const { sourceId, sourceStatus } = useLogSourceContext();
|
||||
|
||||
const {
|
||||
criteria: criteriaErrors,
|
||||
threshold: thresholdErrors,
|
||||
timeSizeUnit: timeSizeUnitErrors,
|
||||
timeWindowSize: timeWindowSizeErrors,
|
||||
} = useMemo(() => decodeOrThrow(errorsRT)(errors), [errors]);
|
||||
|
||||
const supportedFields = useMemo(() => {
|
||||
if (sourceStatus?.logIndexFields) {
|
||||
|
@ -176,7 +190,7 @@ export const Editor: React.FC<Props> = (props) => {
|
|||
);
|
||||
|
||||
const updateCriteria = useCallback(
|
||||
(criteria: AlertParams['criteria']) => {
|
||||
(criteria: PartialCriteriaType) => {
|
||||
setAlertParams('criteria', criteria);
|
||||
},
|
||||
[setAlertParams]
|
||||
|
@ -191,7 +205,9 @@ export const Editor: React.FC<Props> = (props) => {
|
|||
|
||||
const updateTimeUnit = useCallback(
|
||||
(tu: string) => {
|
||||
setAlertParams('timeUnit', tu);
|
||||
if (timeUnitRT.is(tu)) {
|
||||
setAlertParams('timeUnit', tu);
|
||||
}
|
||||
},
|
||||
[setAlertParams]
|
||||
);
|
||||
|
@ -203,20 +219,31 @@ export const Editor: React.FC<Props> = (props) => {
|
|||
[setAlertParams]
|
||||
);
|
||||
|
||||
const defaultCountAlertParams = useMemo(() => createDefaultCountAlertParams(supportedFields), [
|
||||
supportedFields,
|
||||
]);
|
||||
|
||||
const updateType = useCallback(
|
||||
(type: ThresholdType) => {
|
||||
const defaults = type === 'count' ? DEFAULT_COUNT_EXPRESSION : DEFAULT_RATIO_EXPRESSION;
|
||||
const defaults =
|
||||
type === 'count' ? defaultCountAlertParams : createDefaultRatioAlertParams(supportedFields);
|
||||
// Reset properties that don't make sense switching from one context to the other
|
||||
for (const [key, value] of Object.entries({
|
||||
criteria: defaults.criteria,
|
||||
count: defaults.count,
|
||||
})) {
|
||||
setAlertParams(key, value);
|
||||
}
|
||||
setAlertParams('count', defaults.count);
|
||||
setAlertParams('criteria', defaults.criteria);
|
||||
},
|
||||
[setAlertParams]
|
||||
[defaultCountAlertParams, setAlertParams, supportedFields]
|
||||
);
|
||||
|
||||
useMount(() => {
|
||||
const newAlertParams = { ...defaultCountAlertParams, ...alertParams };
|
||||
for (const [key, value] of Object.entries(newAlertParams) as ObjectEntries<
|
||||
typeof newAlertParams
|
||||
>) {
|
||||
setAlertParams(key, value);
|
||||
}
|
||||
setHasSetDefaults(true);
|
||||
});
|
||||
|
||||
// Wait until the alert param defaults have been set
|
||||
if (!hasSetDefaults) return null;
|
||||
|
||||
|
@ -224,7 +251,8 @@ export const Editor: React.FC<Props> = (props) => {
|
|||
<Criteria
|
||||
fields={supportedFields}
|
||||
criteria={alertParams.criteria}
|
||||
errors={errors.criteria}
|
||||
defaultCriterion={defaultCountAlertParams.criteria[0]}
|
||||
errors={criteriaErrors}
|
||||
alertParams={alertParams}
|
||||
sourceId={sourceId}
|
||||
updateCriteria={updateCriteria}
|
||||
|
@ -241,7 +269,7 @@ export const Editor: React.FC<Props> = (props) => {
|
|||
comparator={alertParams.count?.comparator}
|
||||
value={alertParams.count?.value}
|
||||
updateThreshold={updateThreshold}
|
||||
errors={errors.threshold}
|
||||
errors={thresholdErrors}
|
||||
/>
|
||||
|
||||
<ForLastExpression
|
||||
|
@ -249,7 +277,7 @@ export const Editor: React.FC<Props> = (props) => {
|
|||
timeWindowUnit={alertParams.timeUnit}
|
||||
onChangeWindowSize={updateTimeSize}
|
||||
onChangeWindowUnit={updateTimeUnit}
|
||||
errors={{ timeWindowSize: errors.timeWindowSize, timeSizeUnit: errors.timeSizeUnit }}
|
||||
errors={{ timeWindowSize: timeWindowSizeErrors, timeSizeUnit: timeSizeUnitErrors }}
|
||||
/>
|
||||
|
||||
<GroupByExpression
|
||||
|
|
|
@ -8,7 +8,7 @@ import React, { useState } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexItem, EuiFlexGroup, EuiPopover, EuiSelect, EuiExpression } from '@elastic/eui';
|
||||
import {
|
||||
AlertParams,
|
||||
PartialCriteria,
|
||||
ThresholdType,
|
||||
isRatioAlert,
|
||||
} from '../../../../../common/alerting/logs/log_threshold/types';
|
||||
|
@ -45,11 +45,11 @@ const getOptions = (): Array<{
|
|||
};
|
||||
|
||||
interface Props {
|
||||
criteria: AlertParams['criteria'];
|
||||
criteria: PartialCriteria;
|
||||
updateType: (type: ThresholdType) => void;
|
||||
}
|
||||
|
||||
const getThresholdType = (criteria: AlertParams['criteria']): ThresholdType => {
|
||||
const getThresholdType = (criteria: PartialCriteria): ThresholdType => {
|
||||
return isRatioAlert(criteria) ? 'ratio' : 'count';
|
||||
};
|
||||
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||
import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../common/alerting/logs/log_threshold/types';
|
||||
import {
|
||||
LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
|
||||
PartialAlertParams,
|
||||
} from '../../../common/alerting/logs/log_threshold/types';
|
||||
import { validateExpression } from './validation';
|
||||
|
||||
export function getAlertType(): AlertTypeModel {
|
||||
export function getAlertType(): AlertTypeModel<PartialAlertParams> {
|
||||
return {
|
||||
id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
|
||||
description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', {
|
||||
|
|
|
@ -5,45 +5,53 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import * as rt from 'io-ts';
|
||||
import { isNumber, isFinite } from 'lodash';
|
||||
import { ValidationResult } from '../../../../triggers_actions_ui/public';
|
||||
import { IErrorObject, ValidationResult } from '../../../../triggers_actions_ui/public';
|
||||
import {
|
||||
AlertParams,
|
||||
Criteria,
|
||||
RatioCriteria,
|
||||
PartialCountCriteria,
|
||||
isRatioAlert,
|
||||
getNumerator,
|
||||
getDenominator,
|
||||
PartialRequiredAlertParams,
|
||||
PartialCriteria,
|
||||
} from '../../../common/alerting/logs/log_threshold/types';
|
||||
|
||||
export interface CriterionErrors {
|
||||
[id: string]: {
|
||||
field: string[];
|
||||
comparator: string[];
|
||||
value: string[];
|
||||
};
|
||||
}
|
||||
export const criterionErrorRT = rt.type({
|
||||
field: rt.array(rt.string),
|
||||
comparator: rt.array(rt.string),
|
||||
value: rt.array(rt.string),
|
||||
});
|
||||
|
||||
export interface Errors {
|
||||
threshold: {
|
||||
value: string[];
|
||||
};
|
||||
export const criterionErrorsRT = rt.record(rt.string, criterionErrorRT);
|
||||
|
||||
export type CriterionErrors = rt.TypeOf<typeof criterionErrorsRT>;
|
||||
|
||||
const alertingErrorRT: rt.Type<IErrorObject> = rt.recursion('AlertingError', () =>
|
||||
rt.record(rt.string, rt.union([rt.string, rt.array(rt.string), alertingErrorRT]))
|
||||
);
|
||||
|
||||
export const errorsRT = rt.type({
|
||||
threshold: rt.type({
|
||||
value: rt.array(rt.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[];
|
||||
}
|
||||
criteria: rt.record(rt.string, criterionErrorsRT),
|
||||
timeWindowSize: rt.array(rt.string),
|
||||
timeSizeUnit: rt.array(rt.string),
|
||||
});
|
||||
|
||||
export type Errors = rt.TypeOf<typeof errorsRT>;
|
||||
|
||||
export function validateExpression({
|
||||
count,
|
||||
criteria,
|
||||
timeSize,
|
||||
timeUnit,
|
||||
}: Partial<AlertParams>): ValidationResult {
|
||||
}: PartialRequiredAlertParams & {
|
||||
criteria: PartialCriteria;
|
||||
}): ValidationResult {
|
||||
const validationResult = { errors: {} };
|
||||
|
||||
// NOTE: In the case of components provided by the Alerting framework the error property names
|
||||
|
@ -79,7 +87,7 @@ export function validateExpression({
|
|||
|
||||
// Criteria validation
|
||||
if (criteria && criteria.length > 0) {
|
||||
const getCriterionErrors = (_criteria: Criteria): CriterionErrors => {
|
||||
const getCriterionErrors = (_criteria: PartialCountCriteria): CriterionErrors => {
|
||||
const _errors: CriterionErrors = {};
|
||||
|
||||
_criteria.forEach((criterion, idx) => {
|
||||
|
@ -114,12 +122,12 @@ export function validateExpression({
|
|||
};
|
||||
|
||||
if (!isRatioAlert(criteria)) {
|
||||
const criteriaErrors = getCriterionErrors(criteria as Criteria);
|
||||
const criteriaErrors = getCriterionErrors(criteria);
|
||||
errors.criteria[0] = criteriaErrors;
|
||||
} else {
|
||||
const numeratorErrors = getCriterionErrors(getNumerator(criteria as RatioCriteria));
|
||||
const numeratorErrors = getCriterionErrors(getNumerator(criteria));
|
||||
errors.criteria[0] = numeratorErrors;
|
||||
const denominatorErrors = getCriterionErrors(getDenominator(criteria as RatioCriteria));
|
||||
const denominatorErrors = getCriterionErrors(getDenominator(criteria));
|
||||
errors.criteria[1] = denominatorErrors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import type { HttpHandler } from 'src/core/public';
|
||||
import {
|
||||
LogIndexField,
|
||||
LogSourceConfiguration,
|
||||
LogSourceConfigurationProperties,
|
||||
LogSourceConfigurationPropertiesPatch,
|
||||
|
@ -20,6 +21,7 @@ import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status';
|
|||
import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration';
|
||||
|
||||
export {
|
||||
LogIndexField,
|
||||
LogSourceConfiguration,
|
||||
LogSourceConfigurationProperties,
|
||||
LogSourceConfigurationPropertiesPatch,
|
||||
|
|
|
@ -23,12 +23,12 @@ import {
|
|||
UngroupedSearchQueryResponseRT,
|
||||
UngroupedSearchQueryResponse,
|
||||
GroupedSearchQueryResponse,
|
||||
AlertParamsRT,
|
||||
alertParamsRT,
|
||||
isRatioAlertParams,
|
||||
hasGroupBy,
|
||||
getNumerator,
|
||||
getDenominator,
|
||||
Criteria,
|
||||
CountCriteria,
|
||||
CountAlertParams,
|
||||
RatioAlertParams,
|
||||
} from '../../../../common/alerting/logs/log_threshold/types';
|
||||
|
@ -67,7 +67,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
|
|||
const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY);
|
||||
|
||||
try {
|
||||
const validatedParams = decodeOrThrow(AlertParamsRT)(params);
|
||||
const validatedParams = decodeOrThrow(alertParamsRT)(params);
|
||||
|
||||
if (!isRatioAlertParams(validatedParams)) {
|
||||
await executeAlert(
|
||||
|
@ -174,7 +174,7 @@ async function executeRatioAlert(
|
|||
}
|
||||
|
||||
const getESQuery = (
|
||||
alertParams: Omit<AlertParams, 'criteria'> & { criteria: Criteria },
|
||||
alertParams: Omit<AlertParams, 'criteria'> & { criteria: CountCriteria },
|
||||
timestampField: string,
|
||||
indexPattern: string
|
||||
) => {
|
||||
|
@ -366,7 +366,7 @@ export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state,
|
|||
};
|
||||
|
||||
export const buildFiltersFromCriteria = (
|
||||
params: Pick<AlertParams, 'timeSize' | 'timeUnit'> & { criteria: Criteria },
|
||||
params: Pick<AlertParams, 'timeSize' | 'timeUnit'> & { criteria: CountCriteria },
|
||||
timestampField: string
|
||||
) => {
|
||||
const { timeSize, timeUnit, criteria } = params;
|
||||
|
@ -417,7 +417,7 @@ export const buildFiltersFromCriteria = (
|
|||
};
|
||||
|
||||
export const getGroupedESQuery = (
|
||||
params: Pick<AlertParams, 'timeSize' | 'timeUnit' | 'groupBy'> & { criteria: Criteria },
|
||||
params: Pick<AlertParams, 'timeSize' | 'timeUnit' | 'groupBy'> & { criteria: CountCriteria },
|
||||
timestampField: string,
|
||||
index: string
|
||||
): object | undefined => {
|
||||
|
@ -475,7 +475,7 @@ export const getGroupedESQuery = (
|
|||
};
|
||||
|
||||
export const getUngroupedESQuery = (
|
||||
params: Pick<AlertParams, 'timeSize' | 'timeUnit'> & { criteria: Criteria },
|
||||
params: Pick<AlertParams, 'timeSize' | 'timeUnit'> & { criteria: CountCriteria },
|
||||
timestampField: string,
|
||||
index: string
|
||||
): object => {
|
||||
|
@ -509,7 +509,7 @@ type Filter = {
|
|||
[key in SupportedESQueryTypes]?: object;
|
||||
};
|
||||
|
||||
const buildFiltersForCriteria = (criteria: Criteria) => {
|
||||
const buildFiltersForCriteria = (criteria: CountCriteria) => {
|
||||
let filters: Filter[] = [];
|
||||
|
||||
criteria.forEach((criterion) => {
|
||||
|
@ -643,7 +643,7 @@ const getGroupedResults = async (
|
|||
return compositeGroupBuckets;
|
||||
};
|
||||
|
||||
const createConditionsMessageForCriteria = (criteria: Criteria) => {
|
||||
const createConditionsMessageForCriteria = (criteria: CountCriteria) => {
|
||||
const parts = criteria.map((criterion, index) => {
|
||||
const { field, comparator, value } = criterion;
|
||||
return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`;
|
||||
|
|
|
@ -8,7 +8,7 @@ import { PluginSetupContract } from '../../../../../alerts/server';
|
|||
import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor';
|
||||
import {
|
||||
LOG_DOCUMENT_COUNT_ALERT_TYPE_ID,
|
||||
AlertParamsRT,
|
||||
alertParamsRT,
|
||||
} from '../../../../common/alerting/logs/log_threshold/types';
|
||||
import { InfraBackendLibs } from '../../infra_types';
|
||||
import { decodeOrThrow } from '../../../../common/runtime_types';
|
||||
|
@ -86,7 +86,7 @@ export async function registerLogThresholdAlertType(
|
|||
}),
|
||||
validate: {
|
||||
params: {
|
||||
validate: (params) => decodeOrThrow(AlertParamsRT)(params),
|
||||
validate: (params) => decodeOrThrow(alertParamsRT)(params),
|
||||
},
|
||||
},
|
||||
defaultActionGroupId: FIRED_ACTIONS.id,
|
||||
|
|
Loading…
Reference in a new issue