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 de5eda4a1f2c..7f6bf9551e2c 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 @@ -23,6 +23,7 @@ interface Aggregation { buckets: Array<{ aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; doc_count: number; + key_as_string: string; }>; }; } @@ -57,17 +58,18 @@ export const evaluateAlert = ( ); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; - return mapValues(currentValues, (values: number | number[] | null) => { - if (isTooManyBucketsPreviewException(values)) throw values; + return mapValues(currentValues, (points: any[] | typeof NaN | null) => { + if (isTooManyBucketsPreviewException(points)) throw points; return { ...criterion, metric: criterion.metric ?? DOCUMENT_COUNT_I18N, - currentValue: Array.isArray(values) ? last(values) : NaN, - shouldFire: Array.isArray(values) - ? values.map((value) => comparisonFunction(value, threshold)) + currentValue: Array.isArray(points) ? last(points)?.value : NaN, + timestamp: Array.isArray(points) ? last(points)?.key : NaN, + shouldFire: Array.isArray(points) + ? points.map((point) => comparisonFunction(point.value, threshold)) : [false], - isNoData: values === null, - isError: isNaN(values), + isNoData: points === null, + isError: isNaN(points), }; }); }) @@ -157,17 +159,20 @@ const getValuesFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state if (aggType === Aggregators.COUNT) { - return buckets.map((bucket) => bucket.doc_count); + return buckets.map((bucket) => ({ key: bucket.key_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 firstValue.value; + return { key: bucket.key_as_string, value: firstValue.value }; }); } - return buckets.map((bucket) => bucket.aggregatedValue.value); + return buckets.map((bucket) => ({ + key: bucket.key_as_string, + value: bucket.aggregatedValue.value, + })); } catch (e) { return NaN; // Error state } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts index 3ad1031f574e..b4fe8f053a44 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts @@ -56,4 +56,26 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { ); }); }); + + describe('handles time', () => { + const end = new Date('2020-07-08T22:07:27.235Z').valueOf(); + const timerange = { + end, + start: end - 5 * 60 * 1000, + }; + const searchBody = getElasticsearchMetricQuery( + expressionParams, + timefield, + undefined, + undefined, + timerange + ); + test('by rounding timestamps to the nearest timeUnit', () => { + const rangeFilter = searchBody.query.bool.filter.find((filter) => + filter.hasOwnProperty('range') + )?.range[timefield]; + expect(rangeFilter?.lte).toBe(1594246020000); + expect(rangeFilter?.gte).toBe(1594245720000); + }); + }); }); 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 15506a30529c..078ca46d42e6 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 @@ -3,9 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, Aggregators } from '../types'; import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds'; +import { roundTimestamp } from '../../../../utils/round_timestamp'; import { getDateHistogramOffset } from '../../../snapshot/query_helpers'; import { createPercentileAggregation } from './create_percentile_aggregation'; @@ -34,12 +36,15 @@ export const getElasticsearchMetricQuery = ( const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); - const to = timeframe ? timeframe.end : Date.now(); + 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 - intervalAsSeconds * 1000 * MINIMUM_BUCKETS; - const from = timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom; + const from = roundTimestamp( + timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom, + timeUnit + ); const offset = getDateHistogramOffset(from, interval); 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 24f4bc2c678b..003a6c3c20e9 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 @@ -94,12 +94,14 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); test('reports expected values to the action context', async () => { + const now = 1577858400000; await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); expect(action.group).toBe('*'); expect(action.reason).toContain('current value is 1'); expect(action.reason).toContain('threshold of 0.75'); expect(action.reason).toContain('test.metric.1'); + expect(action.timestamp).toBe(new Date(now).toISOString()); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 4c02593dd009..bc1cc24f65ee 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -76,11 +76,13 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s } } if (reason) { + const firstResult = first(alertResults); + const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString(); alertInstance.scheduleActions(FIRED_ACTIONS.id, { group, alertState: stateToAlertMessage[nextState], reason, - timestamp: moment().toISOString(), + timestamp, value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue), threshold: mapToConditionsLookup(criteria, (c) => c.threshold), metric: mapToConditionsLookup(criteria, (c) => c.metric), diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index ee2cf94a2fd6..c7e53eb2008f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -12,6 +12,7 @@ const bucketsA = [ { doc_count: 3, aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] }, + key_as_string: new Date(1577858400000).toISOString(), }, ]; diff --git a/x-pack/plugins/infra/server/utils/round_timestamp.ts b/x-pack/plugins/infra/server/utils/round_timestamp.ts new file mode 100644 index 000000000000..9b5ae2ac4019 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/round_timestamp.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Unit } from '@elastic/datemath'; +import moment from 'moment'; + +export const roundTimestamp = (timestamp: number, unit: Unit) => { + const floor = moment(timestamp).startOf(unit).valueOf(); + const ceil = moment(timestamp).add(1, unit).startOf(unit).valueOf(); + if (Math.abs(timestamp - floor) <= Math.abs(timestamp - ceil)) return floor; + return ceil; +};