[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:
Felix Stürmer 2020-12-22 19:36:02 +01:00 committed by GitHub
parent a98550b4f1
commit 24db3a0070
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 255 additions and 182 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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