Skip to content

Commit 5fada1b

Browse files
committed
[Metrics UI] Fix metric threshold preview regression (#107674)
# Conflicts: # x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
1 parent c88a616 commit 5fada1b

File tree

3 files changed

+106
-15
lines changed

3 files changed

+106
-15
lines changed

x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts

+73-9
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
*/
77

88
import { mapValues, first, last, isNaN } from 'lodash';
9+
import moment from 'moment';
910
import { ElasticsearchClient } from 'kibana/server';
1011
import {
1112
isTooManyBucketsPreviewException,
1213
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
1314
} from '../../../../../common/alerting/metrics';
15+
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
16+
import { roundTimestamp } from '../../../../utils/round_timestamp';
1417
import { InfraSource } from '../../../../../common/source_configuration/source_configuration';
1518
import { InfraDatabaseSearchResponse } from '../../../adapters/framework/adapter_types';
1619
import { createAfterKeyHandler } from '../../../../utils/create_afterkey_handler';
@@ -26,6 +29,7 @@ interface Aggregation {
2629
aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
2730
doc_count: number;
2831
to_as_string: string;
32+
from_as_string: string;
2933
key_as_string: string;
3034
}>;
3135
};
@@ -109,14 +113,34 @@ const getMetric: (
109113
filterQuery,
110114
timeframe
111115
) {
112-
const { aggType } = params;
116+
const { aggType, timeSize, timeUnit } = params;
113117
const hasGroupBy = groupBy && groupBy.length;
118+
119+
const interval = `${timeSize}${timeUnit}`;
120+
const intervalAsSeconds = getIntervalInSeconds(interval);
121+
const intervalAsMS = intervalAsSeconds * 1000;
122+
123+
const to = moment(timeframe ? timeframe.end : Date.now())
124+
.add(1, timeUnit)
125+
.startOf(timeUnit)
126+
.valueOf();
127+
128+
// Rate aggregations need 5 buckets worth of data
129+
const minimumBuckets = aggType === Aggregators.RATE ? 5 : 1;
130+
131+
const minimumFrom = to - intervalAsMS * minimumBuckets;
132+
133+
const from = roundTimestamp(
134+
timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom,
135+
timeUnit
136+
);
137+
114138
const searchBody = getElasticsearchMetricQuery(
115139
params,
116140
timefield,
141+
{ start: from, end: to },
117142
hasGroupBy ? groupBy : undefined,
118-
filterQuery,
119-
timeframe
143+
filterQuery
120144
);
121145

122146
try {
@@ -140,7 +164,11 @@ const getMetric: (
140164
...result,
141165
[Object.values(bucket.key)
142166
.map((value) => value)
143-
.join(', ')]: getValuesFromAggregations(bucket, aggType),
167+
.join(', ')]: getValuesFromAggregations(bucket, aggType, {
168+
from,
169+
to,
170+
bucketSizeInMillis: intervalAsMS,
171+
}),
144172
}),
145173
{}
146174
);
@@ -153,7 +181,8 @@ const getMetric: (
153181
return {
154182
[UNGROUPED_FACTORY_KEY]: getValuesFromAggregations(
155183
(result.aggregations! as unknown) as Aggregation,
156-
aggType
184+
aggType,
185+
{ from, to, bucketSizeInMillis: intervalAsMS }
157186
),
158187
};
159188
} catch (e) {
@@ -173,17 +202,35 @@ const getMetric: (
173202
}
174203
};
175204

205+
interface DropPartialBucketOptions {
206+
from: number;
207+
to: number;
208+
bucketSizeInMillis: number;
209+
}
210+
211+
const dropPartialBuckets = ({ from, to, bucketSizeInMillis }: DropPartialBucketOptions) => (
212+
row: {
213+
key: string;
214+
value: number;
215+
} | null
216+
) => {
217+
if (row == null) return null;
218+
const timestamp = new Date(row.key).valueOf();
219+
return timestamp >= from && timestamp + bucketSizeInMillis <= to;
220+
};
221+
176222
const getValuesFromAggregations = (
177223
aggregations: Aggregation,
178-
aggType: MetricExpressionParams['aggType']
224+
aggType: MetricExpressionParams['aggType'],
225+
dropPartialBucketsOptions: DropPartialBucketOptions
179226
) => {
180227
try {
181228
const { buckets } = aggregations.aggregatedIntervals;
182229
if (!buckets.length) return null; // No Data state
183230

184231
if (aggType === Aggregators.COUNT) {
185232
return buckets.map((bucket) => ({
186-
key: bucket.to_as_string,
233+
key: bucket.from_as_string,
187234
value: bucket.doc_count,
188235
}));
189236
}
@@ -192,11 +239,28 @@ const getValuesFromAggregations = (
192239
const values = bucket.aggregatedValue?.values || [];
193240
const firstValue = first(values);
194241
if (!firstValue) return null;
195-
return { key: bucket.to_as_string, value: firstValue.value };
242+
return { key: bucket.from_as_string, value: firstValue.value };
196243
});
197244
}
245+
246+
if (aggType === Aggregators.AVERAGE) {
247+
return buckets.map((bucket) => ({
248+
key: bucket.key_as_string ?? bucket.from_as_string,
249+
value: bucket.aggregatedValue?.value ?? null,
250+
}));
251+
}
252+
253+
if (aggType === Aggregators.RATE) {
254+
return buckets
255+
.map((bucket) => ({
256+
key: bucket.key_as_string ?? bucket.from_as_string,
257+
value: bucket.aggregatedValue?.value ?? null,
258+
}))
259+
.filter(dropPartialBuckets(dropPartialBucketsOptions));
260+
}
261+
198262
return buckets.map((bucket) => ({
199-
key: bucket.key_as_string ?? bucket.to_as_string,
263+
key: bucket.key_as_string ?? bucket.from_as_string,
200264
value: bucket.aggregatedValue?.value ?? null,
201265
}));
202266
} catch (e) {

x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,30 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
5858
);
5959
});
6060
});
61+
62+
describe('when passed a timeframe of 1 hour', () => {
63+
const testTimeframe = {
64+
start: moment().subtract(1, 'hour').valueOf(),
65+
end: moment().valueOf(),
66+
};
67+
const searchBodyWithoutGroupBy = getElasticsearchMetricQuery(
68+
expressionParams,
69+
timefield,
70+
testTimeframe
71+
);
72+
const searchBodyWithGroupBy = getElasticsearchMetricQuery(
73+
expressionParams,
74+
timefield,
75+
testTimeframe,
76+
groupBy
77+
);
78+
test("generates 1 hour's worth of buckets", () => {
79+
// @ts-ignore
80+
expect(searchBodyWithoutGroupBy.aggs.aggregatedIntervals.date_range.ranges.length).toBe(60);
81+
expect(
82+
// @ts-ignore
83+
searchBodyWithGroupBy.aggs.groupings.aggs.aggregatedIntervals.date_range.ranges.length
84+
).toBe(60);
85+
});
86+
});
6187
});

x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,13 @@ export const getElasticsearchMetricQuery = (
9090
aggregatedIntervals: {
9191
date_range: {
9292
field: timefield,
93-
ranges: [
94-
{
95-
from: to - intervalAsMS - deliveryDelay,
96-
to: to - deliveryDelay,
97-
},
98-
],
93+
// Generate an array of buckets, starting at `from` and ending at `to`
94+
// This is usually only necessary for alert previews or rate aggs. Most alert evaluations
95+
// will generate only one bucket from this logic.
96+
ranges: Array.from(Array(Math.floor((to - from) / intervalAsMS)), (_, i) => ({
97+
from: from + intervalAsMS * i - deliveryDelay,
98+
to: from + intervalAsMS * (i + 1) - deliveryDelay,
99+
})),
99100
},
100101
aggregations,
101102
},

0 commit comments

Comments
 (0)