[Actionable Observability] Verify missing groups for Metric Threshold rule before scheduling no-data actions (#144205)

This commit is contained in:
Chris Cowan 2022-11-01 12:21:26 -06:00 committed by GitHub
parent e17aa78872
commit 5286358b28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 287 additions and 59 deletions

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ElasticsearchClient } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { isString, get, identity } from 'lodash';
import type { BucketKey } from './get_data';
import { calculateCurrentTimeframe, createBaseFilters } from './metric_query';
import { MetricExpressionParams } from '../../../../../common/alerting/metrics';
export interface MissingGroupsRecord {
key: string;
bucketKey: BucketKey;
}
export const checkMissingGroups = async (
esClient: ElasticsearchClient,
metricParams: MetricExpressionParams,
indexPattern: string,
groupBy: string | undefined | string[],
filterQuery: string | undefined,
logger: Logger,
timeframe: { start: number; end: number },
missingGroups: MissingGroupsRecord[] = []
): Promise<MissingGroupsRecord[]> => {
if (missingGroups.length === 0) {
return missingGroups;
}
const currentTimeframe = calculateCurrentTimeframe(metricParams, timeframe);
const baseFilters = createBaseFilters(metricParams, currentTimeframe, filterQuery);
const groupByFields = isString(groupBy) ? [groupBy] : groupBy ? groupBy : [];
const searches = missingGroups.flatMap((group) => {
const groupByFilters = Object.values(group.bucketKey).map((key, index) => {
return {
match: {
[groupByFields[index]]: key,
},
};
});
return [
{ index: indexPattern },
{
size: 0,
terminate_after: 1,
track_total_hits: true,
query: {
bool: {
filter: [...baseFilters, ...groupByFilters],
},
},
},
];
});
logger.trace(`Request: ${JSON.stringify({ searches })}`);
const response = await esClient.msearch({ searches });
logger.trace(`Response: ${JSON.stringify(response)}`);
const verifiedMissingGroups = response.responses
.map((resp, index) => {
const total = get(resp, 'hits.total.value', 0) as number;
if (!total) {
return missingGroups[index];
}
return null;
})
.filter(identity) as MissingGroupsRecord[];
return verifiedMissingGroups;
};

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isString } from 'lodash';
import { MissingGroupsRecord } from './check_missing_group';
export const convertStringsToMissingGroupsRecord = (
missingGroups: Array<string | MissingGroupsRecord>
) => {
return missingGroups.map((subject) => {
if (isString(subject)) {
const parts = subject.split(',');
return {
key: subject,
bucketKey: parts.reduce((acc, part, index) => {
return { ...acc, [`groupBy${index}`]: part };
}, {}),
};
}
return subject;
});
};

View file

@ -14,6 +14,7 @@ import { getIntervalInSeconds } from '../../../../../common/utils/get_interval_i
import { DOCUMENT_COUNT_I18N } from '../../common/messages';
import { createTimerange } from './create_timerange';
import { getData } from './get_data';
import { checkMissingGroups, MissingGroupsRecord } from './check_missing_group';
export interface EvaluatedRuleParams {
criteria: MetricExpressionParams[];
@ -29,6 +30,7 @@ export type Evaluation = Omit<MetricExpressionParams, 'metric'> & {
shouldFire: boolean;
shouldWarn: boolean;
isNoData: boolean;
bucketKey: Record<string, string>;
};
export const evaluateRule = async <Params extends EvaluatedRuleParams = EvaluatedRuleParams>(
@ -40,7 +42,7 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
logger: Logger,
lastPeriodEnd?: number,
timeframe?: { start?: number; end: number },
missingGroups: string[] = []
missingGroups: MissingGroupsRecord[] = []
): Promise<Array<Record<string, Evaluation>>> => {
const { criteria, groupBy, filterQuery } = params;
@ -69,12 +71,24 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
lastPeriodEnd
);
for (const missingGroup of missingGroups) {
if (currentValues[missingGroup] == null) {
currentValues[missingGroup] = {
const verifiedMissingGroups = await checkMissingGroups(
esClient,
criterion,
config.metricAlias,
groupBy,
filterQuery,
logger,
calculatedTimerange,
missingGroups
);
for (const missingGroup of verifiedMissingGroups) {
if (currentValues[missingGroup.key] == null) {
currentValues[missingGroup.key] = {
value: null,
trigger: false,
warn: false,
bucketKey: missingGroup.bucketKey,
};
}
}
@ -91,6 +105,7 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
shouldFire: result.trigger,
shouldWarn: result.warn,
isNoData: result.value === null,
bucketKey: result.bucketKey,
};
}
}

View file

@ -17,10 +17,10 @@ import { getElasticsearchMetricQuery } from './metric_query';
export type GetDataResponse = Record<
string,
{ warn: boolean; trigger: boolean; value: number | null }
{ warn: boolean; trigger: boolean; value: number | null; bucketKey: BucketKey }
>;
type BucketKey = Record<string, string>;
export type BucketKey = Record<string, string>;
interface AggregatedValue {
value: number | null;
values?: Record<string, number | null>;
@ -69,6 +69,7 @@ const NO_DATA_RESPONSE = {
value: null,
warn: false,
trigger: false,
bucketKey: { groupBy0: UNGROUPED_FACTORY_KEY },
},
};
@ -112,6 +113,7 @@ export const getData = async (
trigger: false,
warn: false,
value: null,
bucketKey: bucket.key,
};
} else {
const value =
@ -126,6 +128,7 @@ export const getData = async (
trigger: (shouldTrigger && shouldTrigger.value > 0) || false,
warn: (shouldWarn && shouldWarn.value > 0) || false,
value,
bucketKey: bucket.key,
};
}
}
@ -177,6 +180,7 @@ export const getData = async (
value,
warn,
trigger,
bucketKey: { groupBy0: UNGROUPED_FACTORY_KEY },
},
};
}
@ -185,6 +189,7 @@ export const getData = async (
value,
warn: (shouldWarn && shouldWarn.value > 0) || false,
trigger: (shouldTrigger && shouldTrigger.value > 0) || false,
bucketKey: { groupBy0: UNGROUPED_FACTORY_KEY },
},
};
} else {

View file

@ -12,11 +12,56 @@ import { createPercentileAggregation } from './create_percentile_aggregation';
import { createRateAggsBuckets, createRateAggsBucketScript } from './create_rate_aggregation';
import { wrapInCurrentPeriod } from './wrap_in_period';
const getParsedFilterQuery: (filterQuery: string | undefined) => Record<string, any> | null = (
const getParsedFilterQuery: (filterQuery: string | undefined) => Array<Record<string, any>> = (
filterQuery
) => {
if (!filterQuery) return null;
return JSON.parse(filterQuery);
if (!filterQuery) return [];
return [JSON.parse(filterQuery)];
};
export const calculateCurrentTimeframe = (
metricParams: MetricExpressionParams,
timeframe: { start: number; end: number }
) => ({
...timeframe,
start: moment(timeframe.end)
.subtract(
metricParams.aggType === Aggregators.RATE ? metricParams.timeSize * 2 : metricParams.timeSize,
metricParams.timeUnit
)
.valueOf(),
});
export const createBaseFilters = (
metricParams: MetricExpressionParams,
timeframe: { start: number; end: number },
filterQuery?: string
) => {
const { metric } = metricParams;
const rangeFilters = [
{
range: {
'@timestamp': {
gte: moment(timeframe.start).toISOString(),
lte: moment(timeframe.end).toISOString(),
},
},
},
];
const metricFieldFilters = metric
? [
{
exists: {
field: metric,
},
},
]
: [];
const parsedFilterQuery = getParsedFilterQuery(filterQuery);
return [...rangeFilters, ...metricFieldFilters, ...parsedFilterQuery];
};
export const getElasticsearchMetricQuery = (
@ -39,17 +84,7 @@ export const getElasticsearchMetricQuery = (
// We need to make a timeframe that represents the current timeframe as oppose
// to the total timeframe (which includes the last period).
const currentTimeframe = {
...timeframe,
start: moment(timeframe.end)
.subtract(
metricParams.aggType === Aggregators.RATE
? metricParams.timeSize * 2
: metricParams.timeSize,
metricParams.timeUnit
)
.valueOf(),
};
const currentTimeframe = calculateCurrentTimeframe(metricParams, timeframe);
const metricAggregations =
aggType === Aggregators.COUNT
@ -129,38 +164,13 @@ export const getElasticsearchMetricQuery = (
aggs.groupings.composite.after = afterKey;
}
const rangeFilters = [
{
range: {
'@timestamp': {
gte: moment(timeframe.start).toISOString(),
lte: moment(timeframe.end).toISOString(),
},
},
},
];
const metricFieldFilters = metric
? [
{
exists: {
field: metric,
},
},
]
: [];
const parsedFilterQuery = getParsedFilterQuery(filterQuery);
const baseFilters = createBaseFilters(metricParams, timeframe, filterQuery);
return {
track_total_hits: true,
query: {
bool: {
filter: [
...rangeFilters,
...metricFieldFilters,
...(parsedFilterQuery ? [parsedFilterQuery] : []),
],
filter: baseFilters,
},
},
size: 0,

View file

@ -157,6 +157,7 @@ describe('The metric threshold alert type', () => {
shouldFire,
shouldWarn,
isNoData,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -266,6 +267,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -277,6 +279,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -297,6 +300,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -308,6 +312,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -328,6 +333,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -339,6 +345,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -359,6 +366,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -370,6 +378,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -390,6 +399,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -401,6 +411,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
c: {
...baseNonCountCriterion,
@ -412,6 +423,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'c' },
},
},
]);
@ -429,6 +441,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -440,6 +453,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
c: {
...baseNonCountCriterion,
@ -451,6 +465,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'c' },
},
},
]);
@ -461,7 +476,9 @@ describe('The metric threshold alert type', () => {
'test.metric.1',
stateResult1
);
expect(stateResult2.missingGroups).toEqual(expect.arrayContaining(['c']));
expect(stateResult2.missingGroups).toEqual(
expect.arrayContaining([{ key: 'c', bucketKey: { groupBy0: 'c' } }])
);
setEvaluationResults([
{
a: {
@ -474,6 +491,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -485,6 +503,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -535,6 +554,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -546,6 +566,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
c: {
...baseNonCountCriterion,
@ -557,6 +578,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'c' },
},
},
]);
@ -579,6 +601,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -590,6 +613,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
c: {
...baseNonCountCriterion,
@ -601,6 +625,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'c' },
},
},
]);
@ -611,7 +636,9 @@ describe('The metric threshold alert type', () => {
'test.metric.1',
stateResult1
);
expect(stateResult2.missingGroups).toEqual(expect.arrayContaining(['c']));
expect(stateResult2.missingGroups).toEqual(
expect.arrayContaining([{ key: 'c', bucketKey: { groupBy0: 'c' } }])
);
setEvaluationResults([
{
a: {
@ -624,6 +651,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -635,6 +663,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -692,6 +721,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
{
@ -705,6 +735,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -725,6 +756,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
{},
@ -746,6 +778,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -757,6 +790,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
{
@ -770,6 +804,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -781,6 +816,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -803,6 +839,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
{
@ -816,6 +853,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -867,6 +905,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
},
]);
@ -884,6 +923,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
},
]);
@ -929,6 +969,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseCountCriterion,
@ -940,6 +981,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -958,6 +1000,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseCountCriterion,
@ -969,6 +1012,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -1010,6 +1054,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1027,6 +1072,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1067,6 +1113,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1084,6 +1131,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1124,6 +1172,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1143,6 +1192,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1200,6 +1250,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1217,6 +1268,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1234,6 +1286,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -1245,6 +1298,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -1270,6 +1324,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -1281,6 +1336,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -1302,6 +1358,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -1313,6 +1370,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
c: {
...baseNonCountCriterion,
@ -1324,6 +1382,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'c' },
},
},
]);
@ -1344,6 +1403,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -1355,6 +1415,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -1405,6 +1466,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1422,6 +1484,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -1439,6 +1502,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -1450,6 +1514,7 @@ describe('The metric threshold alert type', () => {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -1473,6 +1538,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'a' },
},
b: {
...baseNonCountCriterion,
@ -1484,6 +1550,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'b' },
},
},
]);
@ -1563,6 +1630,7 @@ describe('The metric threshold alert type', () => {
shouldFire: false,
shouldWarn,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);

View file

@ -34,11 +34,13 @@ import {
} from '../common/utils';
import { EvaluatedRuleParams, evaluateRule } from './lib/evaluate_rule';
import { MissingGroupsRecord } from './lib/check_missing_group';
import { convertStringsToMissingGroupsRecord } from './lib/convert_strings_to_missing_groups_record';
export type MetricThresholdRuleParams = Record<string, any>;
export type MetricThresholdRuleTypeState = RuleTypeState & {
lastRunTimestamp?: number;
missingGroups?: string[];
missingGroups?: Array<string | MissingGroupsRecord>;
groupBy?: string | string[];
filterQuery?: string;
};
@ -144,7 +146,9 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
const filterQueryIsSame = isEqual(state.filterQuery, params.filterQuery);
const groupByIsSame = isEqual(state.groupBy, params.groupBy);
const previousMissingGroups =
alertOnGroupDisappear && filterQueryIsSame && groupByIsSame ? state.missingGroups : [];
alertOnGroupDisappear && filterQueryIsSame && groupByIsSame && state.missingGroups
? state.missingGroups
: [];
const alertResults = await evaluateRule(
services.scopedClusterClient.asCurrentUser,
@ -155,7 +159,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
logger,
state.lastRunTimestamp,
{ end: startedAt.valueOf() },
previousMissingGroups
convertStringsToMissingGroupsRecord(previousMissingGroups)
);
const resultGroupSet = new Set<string>();
@ -166,7 +170,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
}
const groups = [...resultGroupSet];
const nextMissingGroups = new Set<string>();
const nextMissingGroups = new Set<MissingGroupsRecord>();
const hasGroups = !isEqual(groups, [UNGROUPED_FACTORY_KEY]);
let scheduledActionsCount = 0;
@ -180,7 +184,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
const isNoData = alertResults.some((result) => result[group]?.isNoData);
if (isNoData && group !== UNGROUPED_FACTORY_KEY) {
nextMissingGroups.add(group);
nextMissingGroups.add({ key: group, bucketKey: alertResults[0][group].bucketKey });
}
const nextState = isNoData

View file

@ -125,6 +125,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -174,6 +175,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'web' },
},
},
]);
@ -206,7 +208,7 @@ export default function ({ getService }: FtrProviderContext) {
logger,
void 0,
timeFrame,
['middleware']
[{ key: 'middleware', bucketKey: { groupBy0: 'middleware' } }]
);
expect(results).to.eql([
{
@ -222,6 +224,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'web' },
},
middleware: {
timeSize: 5,
@ -235,6 +238,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'middleware' },
},
},
]);
@ -281,6 +285,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -312,6 +317,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -358,6 +364,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -388,7 +395,10 @@ export default function ({ getService }: FtrProviderContext) {
logger,
void 0,
timeFrame,
['web', 'prod']
[
{ key: 'web', bucketKey: { groupBy0: 'web' } },
{ key: 'prod', bucketKey: { groupBy0: 'prod' } },
]
);
expect(results).to.eql([
{
@ -404,6 +414,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: '*' },
},
web: {
timeSize: 5,
@ -417,6 +428,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'web' },
},
prod: {
timeSize: 5,
@ -430,6 +442,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'prod' },
},
},
]);
@ -480,6 +493,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -522,6 +536,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -553,6 +568,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -598,6 +614,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'dev' },
},
prod: {
timeSize: 5,
@ -611,6 +628,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'prod' },
},
},
]);
@ -645,6 +663,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: 'prod' },
},
},
]);
@ -665,7 +684,7 @@ export default function ({ getService }: FtrProviderContext) {
logger,
void 0,
timeFrame,
['dev']
[{ key: 'dev', bucketKey: { groupBy0: 'dev' } }]
);
expect(results).to.eql([
{
@ -681,12 +700,13 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'dev' },
},
},
]);
});
it('should NOT resport any alerts when missing group recovers', async () => {
it('should NOT report any alerts when missing group recovers', async () => {
const params = {
...baseParams,
criteria: [
@ -711,7 +731,7 @@ export default function ({ getService }: FtrProviderContext) {
logger,
moment(gauge.midpoint).subtract(1, 'm').valueOf(),
timeFrame,
['dev']
[{ key: 'dev', bucketKey: { groupBy0: 'dev' } }]
);
expect(results).to.eql([{}]);
});
@ -746,6 +766,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'prod' },
},
dev: {
timeSize: 5,
@ -759,6 +780,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: false,
isNoData: true,
bucketKey: { groupBy0: 'dev' },
},
},
]);
@ -807,6 +829,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -851,6 +874,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: true,
shouldWarn: false,
isNoData: false,
bucketKey: { groupBy0: '*' },
},
},
]);
@ -901,6 +925,7 @@ export default function ({ getService }: FtrProviderContext) {
shouldFire: false,
shouldWarn: true,
isNoData: false,
bucketKey: { groupBy0: 'dev' },
},
},
]);