[Metrics UI] Fix previewing of No Data results (#73753)

This commit is contained in:
Zacqary Adam Xeper 2020-07-30 15:52:32 -05:00 committed by GitHub
parent 5e86d2f848
commit c2d8869cca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 67 additions and 53 deletions

View file

@ -194,7 +194,7 @@ export const AlertPreview: React.FC<Props> = (props) => {
plural: previewResult.resultTotals.noData !== 1 ? 's' : '', plural: previewResult.resultTotals.noData !== 1 ? 's' : '',
}} }}
/> />
) : null} ) : null}{' '}
{previewResult.resultTotals.error ? ( {previewResult.resultTotals.error ? (
<FormattedMessage <FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewErrorResult" id="xpack.infra.metrics.alertFlyout.alertPreviewErrorResult"

View file

@ -368,6 +368,7 @@ export const Expressions: React.FC<Props> = (props) => {
validate={validateMetricThreshold} validate={validateMetricThreshold}
fetch={alertsContext.http.fetch} fetch={alertsContext.http.fetch}
groupByDisplayName={alertParams.nodeType} groupByDisplayName={alertParams.nodeType}
showNoDataResults={alertParams.alertOnNoData}
/> />
<EuiSpacer size={'m'} /> <EuiSpacer size={'m'} />
</> </>

View file

@ -23,9 +23,9 @@ import { InfraSourceConfiguration } from '../../sources';
import { UNGROUPED_FACTORY_KEY } from '../common/utils'; import { UNGROUPED_FACTORY_KEY } from '../common/utils';
type ConditionResult = InventoryMetricConditions & { type ConditionResult = InventoryMetricConditions & {
shouldFire: boolean | boolean[]; shouldFire: boolean[];
currentValue: number; currentValue: number;
isNoData: boolean; isNoData: boolean[];
isError: boolean; isError: boolean;
}; };
@ -71,8 +71,8 @@ export const evaluateCondition = async (
value !== null && value !== null &&
(Array.isArray(value) (Array.isArray(value)
? value.map((v) => comparisonFunction(Number(v), threshold)) ? value.map((v) => comparisonFunction(Number(v), threshold))
: comparisonFunction(value as number, threshold)), : [comparisonFunction(value as number, threshold)]),
isNoData: value === null, isNoData: Array.isArray(value) ? value.map((v) => v === null) : [value === null],
isError: value === undefined, isError: value === undefined,
currentValue: getCurrentValue(value), currentValue: getCurrentValue(value),
}; };

View file

@ -3,7 +3,7 @@
* 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 { first, get } from 'lodash'; import { first, get, last } from 'lodash';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import moment from 'moment'; import moment from 'moment';
import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n';
@ -56,11 +56,14 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
for (const item of inventoryItems) { for (const item of inventoryItems) {
const alertInstance = services.alertInstanceFactory(`${item}`); const alertInstance = services.alertInstanceFactory(`${item}`);
// AND logic; all criteria must be across the threshold // AND logic; all criteria must be across the threshold
const shouldAlertFire = results.every((result) => result[item].shouldFire); const shouldAlertFire = results.every((result) =>
// Grab the result of the most recent bucket
last(result[item].shouldFire)
);
// AND logic; because we need to evaluate all criteria, if one of them reports no data then the // AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state // whole alert is in a No Data/Error state
const isNoData = results.some((result) => result[item].isNoData); const isNoData = results.some((result) => last(result[item].isNoData));
const isError = results.some((result) => result[item].isError); const isError = results.some((result) => result[item].isError);
const nextState = isError const nextState = isError

View file

@ -59,28 +59,29 @@ export const previewInventoryMetricThresholdAlert = async ({
const inventoryItems = Object.keys(first(results) as any); const inventoryItems = Object.keys(first(results) as any);
const previewResults = inventoryItems.map((item) => { 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 numberOfResultBuckets = lookbackSize;
const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution); const numberOfExecutionBuckets = Math.floor(numberOfResultBuckets / alertResultsPerExecution);
return [...Array(numberOfExecutionBuckets)].reduce( let numberOfTimesFired = 0;
(totalFired, _, i) => let numberOfNoDataResults = 0;
totalFired + let numberOfErrors = 0;
(results.every((result) => { for (let i = 0; i < numberOfExecutionBuckets; i++) {
const shouldFire = result[item].shouldFire as boolean[]; const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
return shouldFire[Math.floor(i * alertResultsPerExecution)]; const allConditionsFiredInMappedBucket = results.every((result) => {
}) const shouldFire = result[item].shouldFire as boolean[];
? 1 return shouldFire[mappedBucketIndex];
: 0), });
0 const someConditionsNoDataInMappedBucket = results.some((result) => {
); const hasNoData = result[item].isNoData as boolean[];
return hasNoData[mappedBucketIndex];
});
const someConditionsErrorInMappedBucket = results.some((result) => {
return result[item].isError;
});
if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
if (someConditionsErrorInMappedBucket) numberOfErrors++;
}
return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors];
}); });
return previewResults; return previewResults;

View file

@ -72,7 +72,9 @@ export const evaluateAlert = (
typeof point.value === 'number' && comparisonFunction(point.value, threshold) typeof point.value === 'number' && comparisonFunction(point.value, threshold)
) )
: [false], : [false],
isNoData: (Array.isArray(points) ? last(points)?.value : points) === null, isNoData: Array.isArray(points)
? points.map((point) => point?.value === null || point === null)
: [points === null],
isError: isNaN(Array.isArray(points) ? last(points)?.value : points), isError: isNaN(Array.isArray(points) ? last(points)?.value : points),
}; };
}); });

View file

@ -45,7 +45,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
); );
// AND logic; because we need to evaluate all criteria, if one of them reports no data then the // AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state // whole alert is in a No Data/Error state
const isNoData = alertResults.some((result) => result[group].isNoData); const isNoData = alertResults.some((result) => last(result[group].isNoData));
const isError = alertResults.some((result) => result[group].isError); const isError = alertResults.some((result) => result[group].isError);
const nextState = isError const nextState = isError

View file

@ -36,7 +36,7 @@ export const previewMetricThresholdAlert: (
params: PreviewMetricThresholdAlertParams, params: PreviewMetricThresholdAlertParams,
iterations?: number, iterations?: number,
precalculatedNumberOfGroups?: number precalculatedNumberOfGroups?: number
) => Promise<Array<number | null>> = async ( ) => Promise<number[][]> = async (
{ {
callCluster, callCluster,
params, params,
@ -77,15 +77,6 @@ export const previewMetricThresholdAlert: (
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds; const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
const previewResults = await Promise.all( const previewResults = await Promise.all(
groups.map(async (group) => { groups.map(async (group) => {
const isNoData = alertResults.some((alertResult) => alertResult[group].isNoData);
if (isNoData) {
return null;
}
const isError = alertResults.some((alertResult) => alertResult[group].isError);
if (isError) {
return NaN;
}
// Interpolate the buckets returned by evaluateAlert and return a count of how many of these // Interpolate the buckets returned by evaluateAlert and return a count of how many of these
// buckets would have fired the alert. If the alert interval and bucket interval are the same, // buckets would have fired the alert. If the alert interval and bucket interval are the same,
// this will be a 1:1 evaluation of the alert results. If these are different, the interpolation // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation
@ -95,14 +86,25 @@ export const previewMetricThresholdAlert: (
numberOfResultBuckets / alertResultsPerExecution numberOfResultBuckets / alertResultsPerExecution
); );
let numberOfTimesFired = 0; let numberOfTimesFired = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
for (let i = 0; i < numberOfExecutionBuckets; i++) { for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution); const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
const allConditionsFiredInMappedBucket = alertResults.every( const allConditionsFiredInMappedBucket = alertResults.every(
(alertResult) => alertResult[group].shouldFire[mappedBucketIndex] (alertResult) => alertResult[group].shouldFire[mappedBucketIndex]
); );
const someConditionsNoDataInMappedBucket = alertResults.some((alertResult) => {
const hasNoData = alertResult[group].isNoData as boolean[];
return hasNoData[mappedBucketIndex];
});
const someConditionsErrorInMappedBucket = alertResults.some((alertResult) => {
return alertResult[group].isError;
});
if (allConditionsFiredInMappedBucket) numberOfTimesFired++; if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
if (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
if (someConditionsErrorInMappedBucket) numberOfErrors++;
} }
return numberOfTimesFired; return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors];
}) })
); );
return previewResults; return previewResults;
@ -152,9 +154,9 @@ export const previewMetricThresholdAlert: (
// so filter these results out entirely and only regard the resultA portion // so filter these results out entirely and only regard the resultA portion
.filter((value) => typeof value !== 'undefined') .filter((value) => typeof value !== 'undefined')
.reduce((a, b) => { .reduce((a, b) => {
if (typeof a !== 'number') return a; if (!a) return b;
if (typeof b !== 'number') return b; if (!b) return a;
return a + b; return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
}) })
); );
return zippedResult as any; return zippedResult as any;

View file

@ -55,10 +55,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
const numberOfGroups = previewResult.length; const numberOfGroups = previewResult.length;
const resultTotals = previewResult.reduce( const resultTotals = previewResult.reduce(
(totals, groupResult) => { (totals, [firedResult, noDataResult, errorResult]) => {
if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; return {
if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; ...totals,
return { ...totals, fired: totals.fired + groupResult }; fired: totals.fired + firedResult,
noData: totals.noData + noDataResult,
error: totals.error + errorResult,
};
}, },
{ {
fired: 0, fired: 0,
@ -66,7 +69,6 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
error: 0, error: 0,
} }
); );
return response.ok({ return response.ok({
body: alertPreviewSuccessResponsePayloadRT.encode({ body: alertPreviewSuccessResponsePayloadRT.encode({
numberOfGroups, numberOfGroups,
@ -86,10 +88,13 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
const numberOfGroups = previewResult.length; const numberOfGroups = previewResult.length;
const resultTotals = previewResult.reduce( const resultTotals = previewResult.reduce(
(totals, groupResult) => { (totals, [firedResult, noDataResult, errorResult]) => {
if (groupResult === null) return { ...totals, noData: totals.noData + 1 }; return {
if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 }; ...totals,
return { ...totals, fired: totals.fired + groupResult }; fired: totals.fired + firedResult,
noData: totals.noData + noDataResult,
error: totals.error + errorResult,
};
}, },
{ {
fired: 0, fired: 0,