Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Infrastructure UI][Rules] Refactor Metric Threshold rule to push evaluations to Elasticsearch #126214

Merged
merged 32 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0fc3739
[Infrastructure UI] Refactor rate aggregations for Metric Threshold R…
simianhacker Feb 14, 2022
facd4ea
fixing linting issues
simianhacker Feb 14, 2022
9f18f18
fixing linting issues
simianhacker Feb 14, 2022
bdef278
fixing test
simianhacker Feb 15, 2022
99cde2c
removing unused translations
simianhacker Feb 15, 2022
469d669
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Feb 15, 2022
dea1db4
Fixing timerange
simianhacker Feb 16, 2022
17d7991
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Mar 1, 2022
7317156
[Infrastructure UI] Refactor missing group by tracking for Metrics Th…
simianhacker Feb 22, 2022
d511162
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Mar 9, 2022
d8d4237
Adding null checks for bucket_scripts before using them in evaluations
simianhacker Mar 9, 2022
b605d66
Fixing rate aggs, fixing value for rate aggs, fixing timestamps
simianhacker Mar 9, 2022
f37a954
Fixing function params; removing unused deps
simianhacker Mar 10, 2022
3207637
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Mar 10, 2022
115d259
Fixing metrics alerting test; removing unused deps
simianhacker Mar 10, 2022
27d1a24
Fixing test to handle the new NO_DATA_ACTION; converting test to use …
simianhacker Mar 10, 2022
75273d8
Fixing tests to be more representitive of reality
simianhacker Mar 10, 2022
d3307e6
removing unused deps
simianhacker Mar 10, 2022
11633de
Convert from boolean[] to boolean
simianhacker Mar 11, 2022
f3d921e
Revert "Convert from boolean[] to booleana", made changes to wrong
simianhacker Mar 11, 2022
130daa9
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Mar 16, 2022
18ed8cb
Removing unused translations
simianhacker Mar 16, 2022
e266563
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Mar 16, 2022
d50eaa5
Using startedAt instead of moment() or new Date()
simianhacker Mar 17, 2022
6fc0928
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Mar 18, 2022
fd13165
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Mar 31, 2022
611ee5c
update mocklibs with fake basePath object
simianhacker Apr 7, 2022
4838d79
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Apr 7, 2022
ccdb6dc
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker Apr 29, 2022
da633bd
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Apr 29, 2022
d129cfb
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker May 2, 2022
fbc78f8
Merge branch 'main' of github.com:elastic/kibana into issue-118820-re…
simianhacker May 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,6 @@ export const Expressions: React.FC<Props> = (props) => {
[onFilterChange]
);

const areAllAggsRate = useMemo(
() => ruleParams.criteria?.every((c) => c.aggType === Aggregators.RATE),
[ruleParams.criteria]
);

const hasGroupBy = useMemo(
() => ruleParams.groupBy && ruleParams.groupBy.length > 0,
[ruleParams.groupBy]
Expand Down Expand Up @@ -387,31 +382,6 @@ export const Expressions: React.FC<Props> = (props) => {
checked={ruleParams.alertOnNoData}
onChange={(e) => setRuleParams('alertOnNoData', e.target.checked)}
/>
<EuiCheckbox
id="metrics-alert-partial-buckets-toggle"
label={
<>
{i18n.translate('xpack.infra.metrics.alertFlyout.shouldDropPartialBuckets', {
defaultMessage: 'Drop partial buckets when evaluating data',
})}{' '}
<EuiToolTip
content={i18n.translate(
'xpack.infra.metrics.alertFlyout.dropPartialBucketsHelpText',
{
defaultMessage:
"Enable this to drop the most recent bucket of evaluation data if it's less than {timeSize}{timeUnit}.",
values: { timeSize, timeUnit },
}
)}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</>
}
checked={areAllAggsRate || ruleParams.shouldDropPartialBuckets}
disabled={areAllAggsRate}
onChange={(e) => setRuleParams('shouldDropPartialBuckets', e.target.checked)}
/>
</EuiPanel>
</EuiAccordion>
<EuiSpacer size={'m'} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { InfraTimerangeInput } from '../../../../../common/http_api';

export const calculateRateTimeranges = (timerange: InfraTimerangeInput) => {
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
@@ -0,0 +1,118 @@
/*
* 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 {
Aggregators,
Comparator,
MetricExpressionParams,
} from '../../../../../common/alerting/metrics';
import { createConditionScript } from './create_condition_script';
import { createLastPeriod } from './wrap_in_period';

const EMPTY_SHOULD_WARN = {
bucket_script: {
buckets_path: {},
script: '0',
},
};

export const createBucketSelector = (
condition: MetricExpressionParams,
alertOnGroupDisappear: boolean = false,
groupBy?: string | string[],
lastPeriodEnd?: number
) => {
const hasGroupBy = groupBy != null;
const hasWarn = condition.warningThreshold != null && condition.warningComparator != null;
const isPercentile = [Aggregators.P95, Aggregators.P99].includes(condition.aggType);
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
? {
bucket_script: {
buckets_path: {
value: bucketPath,
},
script: createConditionScript(
condition.warningThreshold as number[],
condition.warningComparator as Comparator
),
},
}
: EMPTY_SHOULD_WARN;

const shouldTrigger = {
bucket_script: {
buckets_path: {
value: bucketPath,
},
script: createConditionScript(condition.threshold, condition.comparator),
},
};

const aggs: any = {
shouldWarn,
shouldTrigger,
};

if (hasGroupBy && alertOnGroupDisappear && lastPeriodEnd) {
const wrappedPeriod = createLastPeriod(lastPeriodEnd, condition);
aggs.lastPeriod = wrappedPeriod.lastPeriod;
aggs.missingGroup = {
bucket_script: {
buckets_path: {
lastPeriod: 'lastPeriod>_count',
currentPeriod: 'currentPeriod>_count',
},
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',
},
};
}

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

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

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

return aggs;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 { Comparator } from '../../../../../common/alerting/metrics';

export const createConditionScript = (threshold: number[], comparator: Comparator) => {
if (comparator === Comparator.BETWEEN && threshold.length === 2) {
return `params.value > ${threshold[0]} && params.value < ${threshold[1]} ? 1 : 0`;
}
if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) {
return `params.value < ${threshold[0]} && params.value > ${threshold[1]} ? 1 : 0`;
}
return `params.value ${comparator} ${threshold[0]} ? 1 : 0`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const createPercentileAggregation = (
percentiles: {
field,
percents: [value],
keyed: false,
keyed: true,
},
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 moment from 'moment';
import { calculateRateTimeranges } from '../../inventory_metric_threshold/lib/calculate_rate_timeranges';
import { TIMESTAMP_FIELD } from '../../../../../common/constants';

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
) => {
const { firstBucketRange, secondBucketRange } = calculateRateTimeranges({
to: timeframe.end,
from: timeframe.start,
});

return {
[`${id}_first_bucket`]: {
filter: {
range: {
[TIMESTAMP_FIELD]: {
gte: moment(firstBucketRange.from).toISOString(),
lt: moment(firstBucketRange.to).toISOString(),
},
},
},
aggs: { maxValue: { max: { field } } },
},
[`${id}_second_bucket`]: {
filter: {
range: {
[TIMESTAMP_FIELD]: {
gte: moment(secondBucketRange.from).toISOString(),
lt: moment(secondBucketRange.to).toISOString(),
},
},
},
aggs: { maxValue: { max: { field } } },
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,23 @@ describe('createTimerange(interval, aggType, timeframe)', () => {
describe('Rate Aggs', () => {
it('should return a 20 second range for last 1 second', () => {
const subject = createTimerange(1000, Aggregators.RATE);
expect(subject.end - subject.start).toEqual(1000 * 5);
expect(subject.end - subject.start).toEqual(1000 * 2);
});
it('should return a 5 minute range for last 1 minute', () => {
const subject = createTimerange(60000, Aggregators.RATE);
expect(subject.end - subject.start).toEqual(60000 * 5);
expect(subject.end - subject.start).toEqual(60000 * 2);
});
it('should return 25 minute range for last 5 minutes', () => {
const subject = createTimerange(300000, Aggregators.RATE);
expect(subject.end - subject.start).toEqual(300000 * 5);
expect(subject.end - subject.start).toEqual(300000 * 2);
});
it('should return 5 hour range for last hour', () => {
const subject = createTimerange(3600000, Aggregators.RATE);
expect(subject.end - subject.start).toEqual(3600000 * 5);
expect(subject.end - subject.start).toEqual(3600000 * 2);
});
it('should return a 5 day range for last day', () => {
const subject = createTimerange(86400000, Aggregators.RATE);
expect(subject.end - subject.start).toEqual(86400000 * 5);
expect(subject.end - subject.start).toEqual(86400000 * 2);
});
});
});
Expand All @@ -78,23 +78,23 @@ describe('createTimerange(interval, aggType, timeframe)', () => {
});
});
describe('Rate Aggs', () => {
it('should return 25 minute range when given 4 minute timeframe', () => {
it('should return 8 minute range when given 4 minute timeframe', () => {
const end = moment();
const timeframe = {
start: end.clone().subtract(4, 'minutes').valueOf(),
end: end.valueOf(),
};
const subject = createTimerange(300000, Aggregators.RATE, timeframe);
expect(subject.end - subject.start).toEqual(300000 * 5);
expect(subject.end - subject.start).toEqual(300000 * 2);
});
it('should return 25 minute range when given 6 minute timeframe', () => {
it('should return 12 minute range when given 6 minute timeframe', () => {
const end = moment();
const timeframe = {
start: end.clone().subtract(6, 'minutes').valueOf(),
end: end.valueOf(),
};
const subject = createTimerange(300000, Aggregators.RATE, timeframe);
expect(subject.end - subject.start).toEqual(300000 * 5);
expect(subject.end - subject.start).toEqual(300000 * 2);
});
});
});
Expand All @@ -113,7 +113,7 @@ describe('createTimerange(interval, aggType, timeframe)', () => {
});
});
describe('Rate Aggs', () => {
it('should return 25 minute range for last 5 minutes', () => {
it('should return 10 minute range for last 5 minutes', () => {
const end = moment();
const timeframe = {
end: end.valueOf(),
Expand All @@ -122,7 +122,7 @@ describe('createTimerange(interval, aggType, timeframe)', () => {
expect(subject).toEqual({
start: end
.clone()
.subtract(300 * 5, 'seconds')
.subtract(300 * 2, 'seconds')
.valueOf(),
end: end.valueOf(),
});
Expand Down
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
Loading