Skip to content

Commit

Permalink
[Infrastructure UI] Refactor missing group by tracking for Metrics Th…
Browse files Browse the repository at this point in the history
…reshold rule
  • Loading branch information
simianhacker committed Feb 22, 2022
1 parent 469d669 commit 0a71ac3
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export const calculateRateTimeranges = (timerange: { to: number; from: number })
// This is the total number of milliseconds for the entire timerange
const totalTime = timerange.to - timerange.from;
// Halfway is the to minus half the total time;
const halfway = timerange.to - totalTime / 2;
const halfway = Math.round(timerange.to - totalTime / 2);
// The interval is half the total time (divided by 1000 to convert to seconds)
const intervalInSeconds = totalTime / 2000;
const intervalInSeconds = Math.round(totalTime / (2 * 1000));

// The first bucket is from the beginning of the time range to the halfway point
const firstBucketRange = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
MetricExpressionParams,
} from '../../../../../common/alerting/metrics';
import { createConditionScript } from './create_condition_script';
import { createLastPeriod } from './wrap_in_period';

const EMPTY_SHOULD_WARN = {
bucket_script: {
Expand All @@ -21,13 +22,20 @@ const EMPTY_SHOULD_WARN = {

export const createBucketSelector = (
condition: MetricExpressionParams,
alertOnGroupDisappear: boolean = false
alertOnGroupDisappear: boolean = false,
lastPeriodEnd?: number
) => {
const hasWarn = condition.warningThreshold != null && condition.warningComparator != null;
const isPercentile = [Aggregators.P95, Aggregators.P99].includes(condition.aggType);
const bucketPath = isPercentile
? `aggregatedValue[${condition.aggType === Aggregators.P95 ? '95' : '99'}]`
: 'aggregatedValue';
const isCount = condition.aggType === Aggregators.COUNT;
const isRate = condition.aggType === Aggregators.RATE;
const bucketPath = isCount
? 'currentPeriod>_count'
: isRate
? `aggregatedValue`
: isPercentile
? `currentPeriod>aggregatedValue[${condition.aggType === Aggregators.P95 ? '95' : '99'}]`
: 'currentPeriod>aggregatedValue';

const shouldWarn = hasWarn
? {
Expand Down Expand Up @@ -57,16 +65,50 @@ export const createBucketSelector = (
shouldTrigger,
};

if (!alertOnGroupDisappear) {
aggs.selectedBucket = {
bucket_selector: {
if (alertOnGroupDisappear && lastPeriodEnd) {
const wrappedPeriod = createLastPeriod(lastPeriodEnd, condition);
aggs.lastPeriod = wrappedPeriod.lastPeriod;
aggs.missingGroup = {
bucket_script: {
buckets_path: {
shouldWarn: 'shouldWarn',
shouldTrigger: 'shouldTrigger',
lastPeriod: 'lastPeriod>_count',
currentPeriod: 'currentPeriod>_count',
},
script: 'params.shouldWarn > 0 || params.shouldTrigger > 0',
script: 'params.lastPeriod > 0 && params.currentPeriod < 1 ? 1 : 0',
},
};
aggs.newOrRecoveredGroup = {
bucket_script: {
buckets_path: {
lastPeriod: 'lastPeriod>_count',
currentPeriod: 'currentPeriod>_count',
},
script: 'params.lastPeriod < 1 && params.currentPeriod > 0 ? 1 : 0',
},
};
}

const evalutionBucketPath =
alertOnGroupDisappear && lastPeriodEnd
? {
shouldWarn: 'shouldWarn',
shouldTrigger: 'shouldTrigger',
missingGroup: 'missingGroup',
newOrRecoveredGroup: 'newOrRecoveredGroup',
}
: { shouldWarn: 'shouldWarn', shouldTrigger: 'shouldTrigger' };

const evaluationScript =
alertOnGroupDisappear && lastPeriodEnd
? 'params.missingGroup > 0 || params.shouldWarn > 0 || params.shouldTrigger > 0 || params.newOrRecoveredGroup > 0'
: 'params.shouldWarn > 0 || params.shouldTrigger > 0';

aggs.evaluation = {
bucket_selector: {
buckets_path: evalutionBucketPath,
script: evaluationScript,
},
};

return aggs;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,32 @@
* 2.0.
*/

import moment from 'moment';
import { calculateRateTimeranges } from '../../inventory_metric_threshold/lib/calculate_rate_timeranges';
import { TIMESTAMP_FIELD } from '../../../../../common/constants';

export const createRateAggs = (
export const createRateAggsBucketScript = (
timeframe: { start: number; end: number },
id: string
) => {
const { intervalInSeconds } = calculateRateTimeranges({
to: timeframe.end,
from: timeframe.start,
});
return {
[id]: {
bucket_script: {
buckets_path: {
first: `currentPeriod>${id}_first_bucket.maxValue`,
second: `currentPeriod>${id}_second_bucket.maxValue`,
},
script: `params.second > 0.0 && params.first > 0.0 && params.second > params.first ? (params.second - params.first) / ${intervalInSeconds}: null`,
},
},
};
};

export const createRateAggsBuckets = (
timeframe: { start: number; end: number },
id: string,
field: string
Expand All @@ -21,10 +44,9 @@ export const createRateAggs = (
[`${id}_first_bucket`]: {
filter: {
range: {
'@timestamp': {
gte: firstBucketRange.from,
lt: firstBucketRange.to,
format: 'epoch_millis',
[TIMESTAMP_FIELD]: {
gte: moment(firstBucketRange.from).toISOString(),
lt: moment(firstBucketRange.to).toISOString(),
},
},
},
Expand All @@ -33,23 +55,13 @@ export const createRateAggs = (
[`${id}_second_bucket`]: {
filter: {
range: {
'@timestamp': {
gte: secondBucketRange.from,
lt: secondBucketRange.to,
format: 'epoch_millis',
[TIMESTAMP_FIELD]: {
gte: moment(secondBucketRange.from).toISOString(),
lt: moment(secondBucketRange.to).toISOString(),
},
},
},
aggs: { maxValue: { max: { field } } },
},
[id]: {
bucket_script: {
buckets_path: {
first: `${id}_first_bucket.maxValue`,
second: `${id}_second_bucket.maxValue`,
},
script: `params.second > 0.0 && params.first > 0.0 && params.second > params.first ? (params.second - params.first) / ${intervalInSeconds}: null`,
},
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import { Aggregators } from '../../../../../common/alerting/metrics';
export const createTimerange = (
interval: number,
aggType: Aggregators,
timeframe?: { end: number; start?: number }
timeframe?: { end: number; start?: number },
lastPeriodEnd?: number
) => {
const to = moment(timeframe ? timeframe.end : Date.now()).valueOf();

// Rate aggregations need 5 buckets worth of data
const minimumBuckets = aggType === Aggregators.RATE ? 5 : 1;

const calculatedFrom = to - interval * minimumBuckets;
const minimumBuckets = aggType === Aggregators.RATE ? 2 : 1;
const calculatedFrom = lastPeriodEnd ? lastPeriodEnd - interval : to - interval * minimumBuckets;

// Use either the timeframe.start when the start is less then calculatedFrom
// OR use the calculatedFrom
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
esClient: ElasticsearchClient,
params: Params,
config: InfraSource['configuration'],
prevGroups: string[],
compositeSize: number,
alertOnGroupDisappear: boolean,
timeframe?: { start?: number; end: number }
lastPeriodEnd?: number,
timeframe?: { start?: number; end: number },
missingGroups: string[] = []
): Promise<Array<Record<string, Evaluation>>> => {
const { criteria, groupBy, filterQuery } = params;

Expand All @@ -48,7 +49,12 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
const interval = `${criterion.timeSize}${criterion.timeUnit}`;
const intervalAsSeconds = getIntervalInSeconds(interval);
const intervalAsMS = intervalAsSeconds * 1000;
const calculatedTimerange = createTimerange(intervalAsMS, criterion.aggType, timeframe);
const calculatedTimerange = createTimerange(
intervalAsMS,
criterion.aggType,
timeframe,
lastPeriodEnd
);

const currentValues = await getData(
esClient,
Expand All @@ -58,42 +64,28 @@ export const evaluateRule = async <Params extends EvaluatedRuleParams = Evaluate
filterQuery,
compositeSize,
alertOnGroupDisappear,
calculatedTimerange
calculatedTimerange,
lastPeriodEnd
);

const backfilledPrevGroups: GetDataResponse = {};
if (alertOnGroupDisappear) {
const currentGroups = Object.keys(currentValues);
const missingGroups = difference(prevGroups, currentGroups);

if (currentGroups.length === 0 && missingGroups.length === 0) {
missingGroups.push(UNGROUPED_FACTORY_KEY);
}
for (const group of missingGroups) {
backfilledPrevGroups[group] = {
// The value use to be set to ZERO for missing groups when using
// Aggregators.COUNT. But that would only trigger if conditions
// matched.
for (const missingGroup of missingGroups) {
if (currentValues[missingGroup] == null) {
currentValues[missingGroup] = {
value: null,
trigger: false,
warn: false,
};
}
}

const currentValuesWithBackfilledPrevGroups = {
...currentValues,
...backfilledPrevGroups,
};

const evaluations: Record<string, Evaluation> = {};
for (const key of Object.keys(currentValuesWithBackfilledPrevGroups)) {
const result = currentValuesWithBackfilledPrevGroups[key];
for (const key of Object.keys(currentValues)) {
const result = currentValues[key];
evaluations[key] = {
...criterion,
metric: criterion.metric ?? DOCUMENT_COUNT_I18N,
currentValue: result.value,
timestamp: moment(calculatedTimerange.start).toISOString(),
timestamp: moment().toISOString(),
shouldFire: result.trigger,
shouldWarn: result.warn,
isNoData: result.value === null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ interface AggregatedValue {
values?: Record<string, number | null>;
}
interface Aggs {
aggregatedValue: AggregatedValue;
currentPeriod: {
doc_count: number;
aggregatedValue?: AggregatedValue;
};
shouldWarn?: {
value: number;
};
shouldTrigger?: {
value: number;
};
missingGroup?: {
value: number;
};
}
interface Bucket extends Aggs {
key: BucketKey;
Expand Down Expand Up @@ -73,6 +79,7 @@ export const getData = async (
compositeSize: number,
alertOnGroupDisappear: boolean,
timeframe: { start: number; end: number },
lastPeriodEnd?: number,
previousResults: GetDataResponse = {},
afterKey?: Record<string, string>
): Promise<GetDataResponse> => {
Expand All @@ -90,12 +97,31 @@ export const getData = async (
const nextAfterKey = groupings.after_key;
for (const bucket of groupings.buckets) {
const key = Object.values(bucket.key).join(',');
const { shouldWarn, shouldTrigger, aggregatedValue } = bucket;
previous[key] = {
trigger: (shouldTrigger && shouldTrigger.value > 0) || false,
warn: (shouldWarn && shouldWarn.value > 0) || false,
value: getValue(aggregatedValue, params),
};
const {
shouldWarn,
shouldTrigger,
missingGroup,
currentPeriod: { aggregatedValue, doc_count: docCount },
} = bucket;
if (missingGroup && missingGroup.value > 0) {
previous[key] = {
trigger: false,
warn: false,
value: null,
};
} else {
const value =
params.aggType === Aggregators.COUNT
? docCount
: aggregatedValue
? getValue(aggregatedValue, params)
: null;
previous[key] = {
trigger: (shouldTrigger && shouldTrigger.value > 0) || false,
warn: (shouldWarn && shouldWarn.value > 0) || false,
value,
};
}
}
if (nextAfterKey) {
return getData(
Expand All @@ -107,15 +133,25 @@ export const getData = async (
compositeSize,
alertOnGroupDisappear,
timeframe,
lastPeriodEnd,
previous,
nextAfterKey
);
}
return previous;
}
if (aggs.all) {
const { aggregatedValue, shouldWarn, shouldTrigger } = aggs.all.buckets.all;
const value = getValue(aggregatedValue, params);
const {
currentPeriod: { aggregatedValue, doc_count: docCount },
shouldWarn,
shouldTrigger,
} = aggs.all.buckets.all;
const value =
params.aggType === Aggregators.COUNT
? docCount
: aggregatedValue
? getValue(aggregatedValue, params)
: null;
// There is an edge case where there is no results and the shouldWarn/shouldTrigger
// bucket scripts will be missing. This is only an issue for document count because
// the value will end up being ZERO, for other metrics it will be null. In this case
Expand Down Expand Up @@ -154,6 +190,7 @@ export const getData = async (
timeframe,
compositeSize,
alertOnGroupDisappear,
lastPeriodEnd,
groupBy,
filterQuery,
afterKey
Expand Down
Loading

0 comments on commit 0a71ac3

Please sign in to comment.