diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index 144ee6505c593..010b769354f04 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -6,6 +6,7 @@ */ import { mapValues, first, last, isNaN } from 'lodash'; +import moment from 'moment'; import { ElasticsearchClient } from 'kibana/server'; import { isTooManyBucketsPreviewException, @@ -111,6 +112,26 @@ const getMetric: ( ) { const { aggType } = params; const hasGroupBy = groupBy && groupBy.length; + + const interval = `${timeSize}${timeUnit}`; + const intervalAsSeconds = getIntervalInSeconds(interval); + const intervalAsMS = intervalAsSeconds * 1000; + + 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( + timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, + timeUnit + ); + const searchBody = getElasticsearchMetricQuery( params, timefield, @@ -180,9 +201,10 @@ 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.to_as_string, + key: bucket.from_as_string, value: bucket.doc_count, })); } @@ -191,11 +213,28 @@ const getValuesFromAggregations = ( const values = bucket.aggregatedValue?.values || []; const firstValue = first(values); if (!firstValue) return null; - return { key: bucket.to_as_string, value: firstValue.value }; + return { key: bucket.from_as_string, value: firstValue.value }; }); } + + if (aggType === Aggregators.AVERAGE) { + return buckets.map((bucket) => ({ + key: bucket.key_as_string ?? bucket.from_as_string, + value: bucket.aggregatedValue?.value ?? null, + })); + } + + 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.to_as_string, + key: bucket.key_as_string ?? bucket.from_as_string, value: bucket.aggregatedValue?.value ?? null, })); } catch (e) { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 0e495c08cc9fd..972b291a3a8cd 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -38,19 +38,10 @@ export const getElasticsearchMetricQuery = ( const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); const intervalAsMS = intervalAsSeconds * 1000; + const to = timeframe.end; + const from = timeframe.start; - const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit); - // We need enough data for 5 buckets worth of data. We also need - // to convert the intervalAsSeconds to milliseconds. - const minimumFrom = to - intervalAsMS * MINIMUM_BUCKETS; - - const from = roundTimestamp( - timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, - timeUnit - ); - - const offset = calculateDateHistogramOffset({ from, to, interval, field: timefield }); - const offsetInMS = parseInt(offset, 10) * 1000; + 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 @@ -74,7 +65,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, @@ -87,10 +78,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, }, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index e5cad99dcb4ed..04e5a3df5822a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -204,7 +204,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 () => { @@ -223,7 +223,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'); }); @@ -247,9 +247,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); }); });