[Metrics UI] Display Too Many Buckets error when previewing Inventory Alerts (#70508)

* [Metrics UI] Display Too Many Buckets error when previewing Inventory Alerts

* Fix typecheck
This commit is contained in:
Zacqary Adam Xeper 2020-07-07 10:05:50 -05:00 committed by GitHub
parent e7c54d3684
commit 5e869b0e77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 103 additions and 74 deletions

View file

@ -5,6 +5,10 @@
*/ */
import { mapValues, last, first } from 'lodash'; import { mapValues, last, first } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import {
isTooManyBucketsPreviewException,
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
} from '../../../../common/alerting/metrics';
import { import {
InfraDatabaseSearchResponse, InfraDatabaseSearchResponse,
CallWithRequestParams, CallWithRequestParams,
@ -57,18 +61,23 @@ export const evaluateCondition = async (
const comparisonFunction = comparatorMap[comparator]; const comparisonFunction = comparatorMap[comparator];
return mapValues(currentValues, (value) => ({ const result = mapValues(currentValues, (value) => {
...condition, if (isTooManyBucketsPreviewException(value)) throw value;
shouldFire: return {
value !== undefined && ...condition,
value !== null && shouldFire:
(Array.isArray(value) value !== undefined &&
? value.map((v) => comparisonFunction(Number(v), threshold)) value !== null &&
: comparisonFunction(value, threshold)), (Array.isArray(value)
isNoData: value === null, ? value.map((v) => comparisonFunction(Number(v), threshold))
isError: value === undefined, : comparisonFunction(value as number, threshold)),
currentValue: getCurrentValue(value), isNoData: value === null,
})); isError: value === undefined,
currentValue: getCurrentValue(value),
};
}) as unknown; // Typescript doesn't seem to know what `throw` is doing
return result as Record<string, ConditionResult>;
}; };
const getCurrentValue: (value: any) => number = (value) => { const getCurrentValue: (value: any) => number = (value) => {
@ -99,21 +108,36 @@ const getData = async (
timerange, timerange,
includeTimeseries: Boolean(timerange.lookbackSize), includeTimeseries: Boolean(timerange.lookbackSize),
}; };
try {
const { nodes } = await snapshot.getNodes(esClient, options);
const { nodes } = await snapshot.getNodes(esClient, options); return nodes.reduce((acc, n) => {
const nodePathItem = last(n.path) as any;
return nodes.reduce((acc, n) => { const m = first(n.metrics);
const nodePathItem = last(n.path) as any; if (m && m.value && m.timeseries) {
const m = first(n.metrics); const { timeseries } = m;
if (m && m.value && m.timeseries) { const values = timeseries.rows.map((row) => row.metric_0) as Array<number | null>;
const { timeseries } = m; acc[nodePathItem.label] = values;
const values = timeseries.rows.map((row) => row.metric_0) as Array<number | null>; } else {
acc[nodePathItem.label] = values; acc[nodePathItem.label] = m && m.value;
} else { }
acc[nodePathItem.label] = m && m.value; return acc;
}, {} as Record<string, number | Array<number | string | null | undefined> | undefined | null>);
} catch (e) {
if (timerange.lookbackSize) {
// This code should only ever be reached when previewing the alert, not executing it
const causedByType = e.body?.error?.caused_by?.type;
if (causedByType === 'too_many_buckets_exception') {
return {
'*': {
[TOO_MANY_BUCKETS_PREVIEW_EXCEPTION]: true,
maxBuckets: e.body.error.caused_by.max_buckets,
},
};
}
} }
return acc; return { '*': undefined };
}, {} as Record<string, number | Array<number | string | null | undefined> | undefined | null>); }
}; };
const comparatorMap = { const comparatorMap = {

View file

@ -6,6 +6,10 @@
import { Unit } from '@elastic/datemath'; import { Unit } from '@elastic/datemath';
import { first } from 'lodash'; import { first } from 'lodash';
import { InventoryMetricConditions } from './types'; import { InventoryMetricConditions } from './types';
import {
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
isTooManyBucketsPreviewException,
} from '../../../../common/alerting/metrics';
import { ILegacyScopedClusterClient } from '../../../../../../../src/core/server'; import { ILegacyScopedClusterClient } from '../../../../../../../src/core/server';
import { InfraSource } from '../../../../common/http_api/source_api'; import { InfraSource } from '../../../../common/http_api/source_api';
import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
@ -46,38 +50,43 @@ export const previewInventoryMetricThresholdAlert = async ({
const alertIntervalInSeconds = getIntervalInSeconds(alertInterval); const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
try {
const results = await Promise.all( const results = await Promise.all(
criteria.map((c) => criteria.map((c) =>
evaluateCondition(c, nodeType, config, callCluster, filterQuery, lookbackSize) evaluateCondition(c, nodeType, config, callCluster, filterQuery, lookbackSize)
) )
);
const inventoryItems = Object.keys(first(results) as any);
const previewResults = inventoryItems.map((item) => {
const isNoData = results.some((result) => result[item].isNoData);
if (isNoData) {
return null;
}
const isError = results.some((result) => result[item].isError);
if (isError) {
return undefined;
}
const numberOfResultBuckets = lookbackSize;
const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution);
return [...Array(numberOfExecutionBuckets)].reduce(
(totalFired, _, i) =>
totalFired +
(results.every((result) => {
const shouldFire = result[item].shouldFire as boolean[];
return shouldFire[Math.floor(i * alertResultsPerExecution)];
})
? 1
: 0),
0
); );
});
return previewResults; const inventoryItems = Object.keys(first(results) as any);
const previewResults = inventoryItems.map((item) => {
const isNoData = results.some((result) => result[item].isNoData);
if (isNoData) {
return null;
}
const isError = results.some((result) => result[item].isError);
if (isError) {
return undefined;
}
const numberOfResultBuckets = lookbackSize;
const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution);
return [...Array(numberOfExecutionBuckets)].reduce(
(totalFired, _, i) =>
totalFired +
(results.every((result) => {
const shouldFire = result[item].shouldFire as boolean[];
return shouldFire[Math.floor(i * alertResultsPerExecution)];
})
? 1
: 0),
0
);
});
return previewResults;
} catch (e) {
if (!isTooManyBucketsPreviewException(e)) throw e;
const { maxBuckets } = e;
throw new Error(`${TOO_MANY_BUCKETS_PREVIEW_EXCEPTION}:${maxBuckets}`);
}
}; };

View file

@ -6,7 +6,6 @@
import { mapValues, first, last, isNaN } from 'lodash'; import { mapValues, first, last, isNaN } from 'lodash';
import { import {
TooManyBucketsPreviewExceptionMetadata,
isTooManyBucketsPreviewException, isTooManyBucketsPreviewException,
TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
} from '../../../../../common/alerting/metrics'; } from '../../../../../common/alerting/metrics';
@ -58,22 +57,19 @@ export const evaluateAlert = (
); );
const { threshold, comparator } = criterion; const { threshold, comparator } = criterion;
const comparisonFunction = comparatorMap[comparator]; const comparisonFunction = comparatorMap[comparator];
return mapValues( return mapValues(currentValues, (values: number | number[] | null) => {
currentValues, if (isTooManyBucketsPreviewException(values)) throw values;
(values: number | number[] | null | TooManyBucketsPreviewExceptionMetadata) => { return {
if (isTooManyBucketsPreviewException(values)) throw values; ...criterion,
return { metric: criterion.metric ?? DOCUMENT_COUNT_I18N,
...criterion, currentValue: Array.isArray(values) ? last(values) : NaN,
metric: criterion.metric ?? DOCUMENT_COUNT_I18N, shouldFire: Array.isArray(values)
currentValue: Array.isArray(values) ? last(values) : NaN, ? values.map((value) => comparisonFunction(value, threshold))
shouldFire: Array.isArray(values) : [false],
? values.map((value) => comparisonFunction(value, threshold)) isNoData: values === null,
: [false], isError: isNaN(values),
isNoData: values === null, };
isError: isNaN(values), });
};
}
);
}) })
); );
}; };