[Metrics UI] Round metric threshold time buckets to nearest unit (#71172)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
3fc54e7c55
commit
66c531d903
|
@ -23,6 +23,7 @@ interface Aggregation {
|
||||||
buckets: Array<{
|
buckets: Array<{
|
||||||
aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
|
aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
|
||||||
doc_count: number;
|
doc_count: number;
|
||||||
|
key_as_string: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -57,17 +58,18 @@ export const evaluateAlert = (
|
||||||
);
|
);
|
||||||
const { threshold, comparator } = criterion;
|
const { threshold, comparator } = criterion;
|
||||||
const comparisonFunction = comparatorMap[comparator];
|
const comparisonFunction = comparatorMap[comparator];
|
||||||
return mapValues(currentValues, (values: number | number[] | null) => {
|
return mapValues(currentValues, (points: any[] | typeof NaN | null) => {
|
||||||
if (isTooManyBucketsPreviewException(values)) throw values;
|
if (isTooManyBucketsPreviewException(points)) throw points;
|
||||||
return {
|
return {
|
||||||
...criterion,
|
...criterion,
|
||||||
metric: criterion.metric ?? DOCUMENT_COUNT_I18N,
|
metric: criterion.metric ?? DOCUMENT_COUNT_I18N,
|
||||||
currentValue: Array.isArray(values) ? last(values) : NaN,
|
currentValue: Array.isArray(points) ? last(points)?.value : NaN,
|
||||||
shouldFire: Array.isArray(values)
|
timestamp: Array.isArray(points) ? last(points)?.key : NaN,
|
||||||
? values.map((value) => comparisonFunction(value, threshold))
|
shouldFire: Array.isArray(points)
|
||||||
|
? points.map((point) => comparisonFunction(point.value, threshold))
|
||||||
: [false],
|
: [false],
|
||||||
isNoData: values === null,
|
isNoData: points === null,
|
||||||
isError: isNaN(values),
|
isError: isNaN(points),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -157,17 +159,20 @@ const getValuesFromAggregations = (
|
||||||
const { buckets } = aggregations.aggregatedIntervals;
|
const { buckets } = aggregations.aggregatedIntervals;
|
||||||
if (!buckets.length) return null; // No Data state
|
if (!buckets.length) return null; // No Data state
|
||||||
if (aggType === Aggregators.COUNT) {
|
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) {
|
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
|
||||||
return buckets.map((bucket) => {
|
return buckets.map((bucket) => {
|
||||||
const values = bucket.aggregatedValue?.values || [];
|
const values = bucket.aggregatedValue?.values || [];
|
||||||
const firstValue = first(values);
|
const firstValue = first(values);
|
||||||
if (!firstValue) return null;
|
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) {
|
} catch (e) {
|
||||||
return NaN; // Error state
|
return NaN; // Error state
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with 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 { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
|
||||||
import { MetricExpressionParams, Aggregators } from '../types';
|
import { MetricExpressionParams, Aggregators } from '../types';
|
||||||
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
|
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
|
||||||
|
import { roundTimestamp } from '../../../../utils/round_timestamp';
|
||||||
import { getDateHistogramOffset } from '../../../snapshot/query_helpers';
|
import { getDateHistogramOffset } from '../../../snapshot/query_helpers';
|
||||||
import { createPercentileAggregation } from './create_percentile_aggregation';
|
import { createPercentileAggregation } from './create_percentile_aggregation';
|
||||||
|
|
||||||
|
@ -34,12 +36,15 @@ export const getElasticsearchMetricQuery = (
|
||||||
const interval = `${timeSize}${timeUnit}`;
|
const interval = `${timeSize}${timeUnit}`;
|
||||||
const intervalAsSeconds = getIntervalInSeconds(interval);
|
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
|
// We need enough data for 5 buckets worth of data. We also need
|
||||||
// to convert the intervalAsSeconds to milliseconds.
|
// to convert the intervalAsSeconds to milliseconds.
|
||||||
const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS;
|
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);
|
const offset = getDateHistogramOffset(from, interval);
|
||||||
|
|
||||||
|
|
|
@ -94,12 +94,14 @@ describe('The metric threshold alert type', () => {
|
||||||
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
|
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
|
||||||
});
|
});
|
||||||
test('reports expected values to the action context', async () => {
|
test('reports expected values to the action context', async () => {
|
||||||
|
const now = 1577858400000;
|
||||||
await execute(Comparator.GT, [0.75]);
|
await execute(Comparator.GT, [0.75]);
|
||||||
const { action } = mostRecentAction(instanceID);
|
const { action } = mostRecentAction(instanceID);
|
||||||
expect(action.group).toBe('*');
|
expect(action.group).toBe('*');
|
||||||
expect(action.reason).toContain('current value is 1');
|
expect(action.reason).toContain('current value is 1');
|
||||||
expect(action.reason).toContain('threshold of 0.75');
|
expect(action.reason).toContain('threshold of 0.75');
|
||||||
expect(action.reason).toContain('test.metric.1');
|
expect(action.reason).toContain('test.metric.1');
|
||||||
|
expect(action.timestamp).toBe(new Date(now).toISOString());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -76,11 +76,13 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (reason) {
|
if (reason) {
|
||||||
|
const firstResult = first(alertResults);
|
||||||
|
const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString();
|
||||||
alertInstance.scheduleActions(FIRED_ACTIONS.id, {
|
alertInstance.scheduleActions(FIRED_ACTIONS.id, {
|
||||||
group,
|
group,
|
||||||
alertState: stateToAlertMessage[nextState],
|
alertState: stateToAlertMessage[nextState],
|
||||||
reason,
|
reason,
|
||||||
timestamp: moment().toISOString(),
|
timestamp,
|
||||||
value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue),
|
value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue),
|
||||||
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
|
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
|
||||||
metric: mapToConditionsLookup(criteria, (c) => c.metric),
|
metric: mapToConditionsLookup(criteria, (c) => c.metric),
|
||||||
|
|
|
@ -12,6 +12,7 @@ const bucketsA = [
|
||||||
{
|
{
|
||||||
doc_count: 3,
|
doc_count: 3,
|
||||||
aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
|
aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
|
||||||
|
key_as_string: new Date(1577858400000).toISOString(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
15
x-pack/plugins/infra/server/utils/round_timestamp.ts
Normal file
15
x-pack/plugins/infra/server/utils/round_timestamp.ts
Normal file
|
@ -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;
|
||||||
|
};
|
Loading…
Reference in a new issue