Skip to content

Commit 17d4653

Browse files
simianhackerkibanamachine
authored andcommitted
[Metrics UI] Drop partial buckets from ALL Metrics UI queries (#104784)
* [Metrics UI] Change dropLastBucket to dropPartialBuckets - Change offset calculation to millisecond percission - Change dropLastBucket to dropPartialBuckets - Impliment partial bucket filter - Adding partial bucket filter to metric threshold alerts * Cleaning up getElasticsearchMetricQuery * Change timestamp to from_as_string to align to how date_histgram works * Fixing tests to be more realistic * fixing types; removing extra imports * Fixing new mock data to work with previews * Removing value checks since they don't really provide much value * Removing test for refactored functinality * Change value to match millisecond resolution * Fixing values for new partial bucket scheme * removing unused var * Fixing lookback since drops more than last buckets * Changing results count * fixing more tests * Removing empty describe Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> # Conflicts: # x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
1 parent b2615e2 commit 17d4653

21 files changed

+432
-144
lines changed

x-pack/plugins/infra/common/http_api/metrics_api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const MetricsAPIRequestRT = rt.intersection([
3535
afterKey: rt.union([rt.null, afterKeyObjectRT]),
3636
limit: rt.union([rt.number, rt.null, rt.undefined]),
3737
filters: rt.array(rt.object),
38-
dropLastBucket: rt.boolean,
38+
dropPartialBuckets: rt.boolean,
3939
alignDataToEnd: rt.boolean,
4040
}),
4141
]);

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

+69-20
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
isTooManyBucketsPreviewException,
1212
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
1313
} from '../../../../../common/alerting/metrics';
14+
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
15+
import { roundTimestamp } from '../../../../utils/round_timestamp';
1416
import { InfraSource } from '../../../../../common/source_configuration/source_configuration';
1517
import { InfraDatabaseSearchResponse } from '../../../adapters/framework/adapter_types';
1618
import { createAfterKeyHandler } from '../../../../utils/create_afterkey_handler';
@@ -26,6 +28,7 @@ interface Aggregation {
2628
aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
2729
doc_count: number;
2830
to_as_string: string;
31+
from_as_string: string;
2932
key_as_string: string;
3033
}>;
3134
};
@@ -92,6 +95,8 @@ export const evaluateAlert = <Params extends EvaluatedAlertParams = EvaluatedAle
9295
);
9396
};
9497

98+
const MINIMUM_BUCKETS = 5;
99+
95100
const getMetric: (
96101
esClient: ElasticsearchClient,
97102
params: MetricExpressionParams,
@@ -109,14 +114,29 @@ const getMetric: (
109114
filterQuery,
110115
timeframe
111116
) {
112-
const { aggType } = params;
117+
const { aggType, timeSize, timeUnit } = params;
113118
const hasGroupBy = groupBy && groupBy.length;
119+
120+
const interval = `${timeSize}${timeUnit}`;
121+
const intervalAsSeconds = getIntervalInSeconds(interval);
122+
const intervalAsMS = intervalAsSeconds * 1000;
123+
124+
const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit);
125+
// We need enough data for 5 buckets worth of data. We also need
126+
// to convert the intervalAsSeconds to milliseconds.
127+
const minimumFrom = to - intervalAsMS * MINIMUM_BUCKETS;
128+
129+
const from = roundTimestamp(
130+
timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom,
131+
timeUnit
132+
);
133+
114134
const searchBody = getElasticsearchMetricQuery(
115135
params,
116136
timefield,
137+
{ start: from, end: to },
117138
hasGroupBy ? groupBy : undefined,
118-
filterQuery,
119-
timeframe
139+
filterQuery
120140
);
121141

122142
try {
@@ -140,7 +160,11 @@ const getMetric: (
140160
...result,
141161
[Object.values(bucket.key)
142162
.map((value) => value)
143-
.join(', ')]: getValuesFromAggregations(bucket, aggType),
163+
.join(', ')]: getValuesFromAggregations(bucket, aggType, {
164+
from,
165+
to,
166+
bucketSizeInMillis: intervalAsMS,
167+
}),
144168
}),
145169
{}
146170
);
@@ -153,7 +177,8 @@ const getMetric: (
153177
return {
154178
[UNGROUPED_FACTORY_KEY]: getValuesFromAggregations(
155179
(result.aggregations! as unknown) as Aggregation,
156-
aggType
180+
aggType,
181+
{ from, to, bucketSizeInMillis: intervalAsMS }
157182
),
158183
};
159184
} catch (e) {
@@ -173,32 +198,56 @@ const getMetric: (
173198
}
174199
};
175200

201+
interface DropPartialBucketOptions {
202+
from: number;
203+
to: number;
204+
bucketSizeInMillis: number;
205+
}
206+
207+
const dropPartialBuckets = ({ from, to, bucketSizeInMillis }: DropPartialBucketOptions) => (
208+
row: {
209+
key: string;
210+
value: number;
211+
} | null
212+
) => {
213+
if (row == null) return null;
214+
const timestamp = new Date(row.key).valueOf();
215+
return timestamp >= from && timestamp + bucketSizeInMillis <= to;
216+
};
217+
176218
const getValuesFromAggregations = (
177219
aggregations: Aggregation,
178-
aggType: MetricExpressionParams['aggType']
220+
aggType: MetricExpressionParams['aggType'],
221+
dropPartialBucketsOptions: DropPartialBucketOptions
179222
) => {
180223
try {
181224
const { buckets } = aggregations.aggregatedIntervals;
182225
if (!buckets.length) return null; // No Data state
183226

184227
if (aggType === Aggregators.COUNT) {
185-
return buckets.map((bucket) => ({
186-
key: bucket.to_as_string,
187-
value: bucket.doc_count,
188-
}));
228+
return buckets
229+
.map((bucket) => ({
230+
key: bucket.from_as_string,
231+
value: bucket.doc_count,
232+
}))
233+
.filter(dropPartialBuckets(dropPartialBucketsOptions));
189234
}
190235
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
191-
return buckets.map((bucket) => {
192-
const values = bucket.aggregatedValue?.values || [];
193-
const firstValue = first(values);
194-
if (!firstValue) return null;
195-
return { key: bucket.to_as_string, value: firstValue.value };
196-
});
236+
return buckets
237+
.map((bucket) => {
238+
const values = bucket.aggregatedValue?.values || [];
239+
const firstValue = first(values);
240+
if (!firstValue) return null;
241+
return { key: bucket.from_as_string, value: firstValue.value };
242+
})
243+
.filter(dropPartialBuckets(dropPartialBucketsOptions));
197244
}
198-
return buckets.map((bucket) => ({
199-
key: bucket.key_as_string ?? bucket.to_as_string,
200-
value: bucket.aggregatedValue?.value ?? null,
201-
}));
245+
return buckets
246+
.map((bucket) => ({
247+
key: bucket.key_as_string ?? bucket.from_as_string,
248+
value: bucket.aggregatedValue?.value ?? null,
249+
}))
250+
.filter(dropPartialBuckets(dropPartialBucketsOptions));
202251
} catch (e) {
203252
return NaN; // Error state
204253
}

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { MetricExpressionParams } from '../types';
99
import { getElasticsearchMetricQuery } from './metric_query';
10+
import moment from 'moment';
1011

1112
describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
1213
const expressionParams = {
@@ -18,9 +19,13 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
1819

1920
const timefield = '@timestamp';
2021
const groupBy = 'host.doggoname';
22+
const timeframe = {
23+
start: moment().subtract(5, 'minutes').valueOf(),
24+
end: moment().valueOf(),
25+
};
2126

2227
describe('when passed no filterQuery', () => {
23-
const searchBody = getElasticsearchMetricQuery(expressionParams, timefield, groupBy);
28+
const searchBody = getElasticsearchMetricQuery(expressionParams, timefield, timeframe, groupBy);
2429
test('includes a range filter', () => {
2530
expect(
2631
searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range'))
@@ -43,6 +48,7 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
4348
const searchBody = getElasticsearchMetricQuery(
4449
expressionParams,
4550
timefield,
51+
timeframe,
4652
groupBy,
4753
filterQuery
4854
);

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

+2-4
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import moment from 'moment';
88
import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
99
import { MetricExpressionParams, Aggregators } from '../types';
1010
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
11-
import { roundTimestamp } from '../../../../utils/round_timestamp';
1211
import { createPercentileAggregation } from './create_percentile_aggregation';
1312
import { calculateDateHistogramOffset } from '../../../metrics/lib/calculate_date_histogram_offset';
1413

15-
const MINIMUM_BUCKETS = 5;
1614
const COMPOSITE_RESULTS_PER_PAGE = 100;
1715

1816
const getParsedFilterQuery: (filterQuery: string | undefined) => Record<string, any> | null = (
@@ -25,9 +23,9 @@ const getParsedFilterQuery: (filterQuery: string | undefined) => Record<string,
2523
export const getElasticsearchMetricQuery = (
2624
{ metric, aggType, timeUnit, timeSize }: MetricExpressionParams,
2725
timefield: string,
26+
timeframe: { start: number; end: number },
2827
groupBy?: string | string[],
29-
filterQuery?: string,
30-
timeframe?: { start: number; end: number }
28+
filterQuery?: string
3129
) => {
3230
if (aggType === Aggregators.COUNT && metric) {
3331
throw new Error('Cannot aggregate document count with a metric');

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

+9-10
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,12 @@ describe('The metric threshold alert type', () => {
119119
expect(mostRecentAction(instanceID)).toBe(undefined);
120120
});
121121
test('reports expected values to the action context', async () => {
122-
const now = 1577858400000;
123122
await execute(Comparator.GT, [0.75]);
124123
const { action } = mostRecentAction(instanceID);
125124
expect(action.group).toBe('*');
126125
expect(action.reason).toContain('current value is 1');
127126
expect(action.reason).toContain('threshold of 0.75');
128127
expect(action.reason).toContain('test.metric.1');
129-
expect(action.timestamp).toBe(new Date(now).toISOString());
130128
});
131129
});
132130

@@ -428,15 +426,13 @@ describe('The metric threshold alert type', () => {
428426
},
429427
});
430428
test('reports values converted from decimals to percentages to the action context', async () => {
431-
const now = 1577858400000;
432429
await execute();
433430
const { action } = mostRecentAction(instanceID);
434431
expect(action.group).toBe('*');
435432
expect(action.reason).toContain('current value is 100%');
436433
expect(action.reason).toContain('threshold of 75%');
437434
expect(action.threshold.condition0[0]).toBe('75%');
438435
expect(action.value.condition0).toBe('100%');
439-
expect(action.timestamp).toBe(new Date(now).toISOString());
440436
});
441437
});
442438
});
@@ -460,7 +456,8 @@ const executor = createMetricThresholdExecutor(mockLibs);
460456

461457
const services: AlertServicesMock = alertsMock.createAlertServices();
462458
services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => {
463-
if (params.index === 'alternatebeat-*') return mocks.changedSourceIdResponse;
459+
const from = params?.body.query.bool.filter[0]?.range['@timestamp'].gte;
460+
if (params.index === 'alternatebeat-*') return mocks.changedSourceIdResponse(from);
464461
const metric = params?.body.query.bool.filter[1]?.exists.field;
465462
if (params?.body.aggs.groupings) {
466463
if (params?.body.aggs.groupings.composite.after) {
@@ -470,25 +467,27 @@ services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: a
470467
}
471468
if (metric === 'test.metric.2') {
472469
return elasticsearchClientMock.createSuccessTransportRequestPromise(
473-
mocks.alternateCompositeResponse
470+
mocks.alternateCompositeResponse(from)
474471
);
475472
}
476473
return elasticsearchClientMock.createSuccessTransportRequestPromise(
477-
mocks.basicCompositeResponse
474+
mocks.basicCompositeResponse(from)
478475
);
479476
}
480477
if (metric === 'test.metric.2') {
481478
return elasticsearchClientMock.createSuccessTransportRequestPromise(
482-
mocks.alternateMetricResponse
479+
mocks.alternateMetricResponse(from)
483480
);
484481
} else if (metric === 'test.metric.3') {
485482
return elasticsearchClientMock.createSuccessTransportRequestPromise(
486-
params?.body.aggs.aggregatedIntervals.aggregations.aggregatedValue_max
483+
params?.body.aggs.aggregatedIntervals.aggregations.aggregatedValueMax
487484
? mocks.emptyRateResponse
488485
: mocks.emptyMetricResponse
489486
);
490487
}
491-
return elasticsearchClientMock.createSuccessTransportRequestPromise(mocks.basicMetricResponse);
488+
return elasticsearchClientMock.createSuccessTransportRequestPromise(
489+
mocks.basicMetricResponse(from)
490+
);
492491
});
493492
services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => {
494493
if (sourceId === 'alternate')

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ describe('Previewing the metric threshold alert type', () => {
167167
const services: AlertServicesMock = alertsMock.createAlertServices();
168168

169169
services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: any): any => {
170+
const from = params?.body.query.bool.filter[0]?.range['@timestamp'].gte;
170171
const metric = params?.body.query.bool.filter[1]?.exists.field;
171172
if (params?.body.aggs.groupings) {
172173
if (params?.body.aggs.groupings.composite.after) {
@@ -175,21 +176,21 @@ services.scopedClusterClient.asCurrentUser.search.mockImplementation((params?: a
175176
);
176177
}
177178
return elasticsearchClientMock.createSuccessTransportRequestPromise(
178-
mocks.basicCompositePreviewResponse
179+
mocks.basicCompositePreviewResponse(from)
179180
);
180181
}
181182
if (metric === 'test.metric.2') {
182183
return elasticsearchClientMock.createSuccessTransportRequestPromise(
183-
mocks.alternateMetricPreviewResponse
184+
mocks.alternateMetricPreviewResponse(from)
184185
);
185186
}
186187
if (metric === 'test.metric.3') {
187188
return elasticsearchClientMock.createSuccessTransportRequestPromise(
188-
mocks.repeatingMetricPreviewResponse
189+
mocks.repeatingMetricPreviewResponse(from)
189190
);
190191
}
191192
return elasticsearchClientMock.createSuccessTransportRequestPromise(
192-
mocks.basicMetricPreviewResponse
193+
mocks.basicMetricPreviewResponse(from)
193194
);
194195
});
195196

0 commit comments

Comments
 (0)