Skip to content

Commit

Permalink
[Metrics UI] Correct inaccurate offsetting for non-rate aggregations …
Browse files Browse the repository at this point in the history
…inside of metrics threshold alerts (#106947)

* Don't skip last bucket for most aggs

* Allow alerting on partial buckets for certain aggs

* Fix test, PR feedback, and some comments

* Remove all offset logic for date_range aggs

* Remove code comment

* Add delivery delay

* Fix the date range for query

* Add TODO
  • Loading branch information
phillipb authored and Zacqary committed Aug 6, 2021
1 parent d089b3c commit 05062e8
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { mapValues, first, last, isNaN } from 'lodash';
import moment from 'moment';
import { ElasticsearchClient } from 'kibana/server';
import {
isTooManyBucketsPreviewException,
Expand Down Expand Up @@ -121,9 +122,14 @@ const getMetric: (
const intervalAsSeconds = getIntervalInSeconds(interval);
const intervalAsMS = intervalAsSeconds * 1000;

const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit);
const to = moment(timeframe ? timeframe.end : Date.now())
.add(1, timeUnit)
.startOf(timeUnit)
.valueOf();

// We need enough data for 5 buckets worth of data. We also need
// to convert the intervalAsSeconds to milliseconds.
// TODO: We only need to get 5 buckets for the rate query, so this logic should move there.
const minimumFrom = to - intervalAsMS * MINIMUM_BUCKETS;

const from = roundTimestamp(
Expand Down Expand Up @@ -223,30 +229,42 @@ const getValuesFromAggregations = (
try {
const { buckets } = aggregations.aggregatedIntervals;
if (!buckets.length) return null; // No Data state

if (aggType === Aggregators.COUNT) {
return buckets
.map((bucket) => ({
key: bucket.from_as_string,
value: bucket.doc_count,
}))
.filter(dropPartialBuckets(dropPartialBucketsOptions));
return buckets.map((bucket) => ({
key: bucket.from_as_string,
value: bucket.doc_count,
}));
}
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
return buckets
.map((bucket) => {
const values = bucket.aggregatedValue?.values || [];
const firstValue = first(values);
if (!firstValue) return null;
return { key: bucket.from_as_string, value: firstValue.value };
})
.filter(dropPartialBuckets(dropPartialBucketsOptions));
return buckets.map((bucket) => {
const values = bucket.aggregatedValue?.values || [];
const firstValue = first(values);
if (!firstValue) return null;
return { key: bucket.from_as_string, value: firstValue.value };
});
}
return buckets
.map((bucket) => ({

if (aggType === Aggregators.AVERAGE) {
return buckets.map((bucket) => ({
key: bucket.key_as_string ?? bucket.from_as_string,
value: bucket.aggregatedValue?.value ?? null,
}))
.filter(dropPartialBuckets(dropPartialBucketsOptions));
}));
}

if (aggType === Aggregators.RATE) {
return buckets
.map((bucket) => ({
key: bucket.key_as_string ?? bucket.from_as_string,
value: bucket.aggregatedValue?.value ?? null,
}))
.filter(dropPartialBuckets(dropPartialBucketsOptions));
}

return buckets.map((bucket) => ({
key: bucket.key_as_string ?? bucket.from_as_string,
value: bucket.aggregatedValue?.value ?? null,
}));
} catch (e) {
return NaN; // Error state
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export const getElasticsearchMetricQuery = (
const intervalAsMS = intervalAsSeconds * 1000;
const to = timeframe.end;
const from = timeframe.start;
const offset = calculateDateHistogramOffset({ from, to, interval, field: timefield });
const offsetInMS = parseInt(offset, 10);

const deliveryDelay = 60 * 1000; // INFO: This allows us to account for any delay ES has in indexing the most recent data.

const aggregations =
aggType === Aggregators.COUNT
Expand All @@ -63,7 +63,7 @@ export const getElasticsearchMetricQuery = (
date_histogram: {
field: timefield,
fixed_interval: interval,
offset,
offset: calculateDateHistogramOffset({ from, to, interval, field: timefield }),
extended_bounds: {
min: from,
max: to,
Expand All @@ -76,10 +76,12 @@ export const getElasticsearchMetricQuery = (
aggregatedIntervals: {
date_range: {
field: timefield,
ranges: Array.from(Array(Math.floor((to - from) / intervalAsMS)), (_, i) => ({
from: from + intervalAsMS * i + offsetInMS,
to: from + intervalAsMS * (i + 1) + offsetInMS,
})),
ranges: [
{
from: to - intervalAsMS - deliveryDelay,
to: to - deliveryDelay,
},
],
},
aggregations,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ describe('The metric threshold alert type', () => {
});
test('sends no alert when some, but not all, criteria cross the threshold', async () => {
const instanceID = '*';
await execute(Comparator.LT_OR_EQ, [1.0], [3.0]);
await execute(Comparator.LT_OR_EQ, [1.0], [2.5]);
expect(mostRecentAction(instanceID)).toBe(undefined);
});
test('alerts only on groups that meet all criteria when querying with a groupBy parameter', async () => {
Expand All @@ -251,7 +251,7 @@ describe('The metric threshold alert type', () => {
expect(reasons[0]).toContain('test.metric.1');
expect(reasons[1]).toContain('test.metric.2');
expect(reasons[0]).toContain('current value is 1');
expect(reasons[1]).toContain('current value is 3.5');
expect(reasons[1]).toContain('current value is 3');
expect(reasons[0]).toContain('threshold of 1');
expect(reasons[1]).toContain('threshold of 3');
});
Expand All @@ -274,9 +274,9 @@ describe('The metric threshold alert type', () => {
},
});
test('alerts based on the doc_count value instead of the aggregatedValue', async () => {
await execute(Comparator.GT, [2]);
await execute(Comparator.GT, [0.9]);
expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id);
await execute(Comparator.LT, [1.5]);
await execute(Comparator.LT, [0.5]);
expect(mostRecentAction(instanceID)).toBe(undefined);
});
});
Expand Down

0 comments on commit 05062e8

Please sign in to comment.