mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Actionable Observability] Verify missing groups for Metric Threshold rule before scheduling no-data actions (#144205)
This commit is contained in:
parent
e17aa78872
commit
5286358b28
8 changed files with 287 additions and 59 deletions
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: '*' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue