[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' : '',
}}
/>
) : null}
) : null}{' '}
{previewResult.resultTotals.error ? (
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewErrorResult"

View file

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

View file

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

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under 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 moment from 'moment';
import { toMetricOpt } from '../../../../common/snapshot_metric_i18n';
@ -56,11 +56,14 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
for (const item of inventoryItems) {
const alertInstance = services.alertInstanceFactory(`${item}`);
// 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
// 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 nextState = isError

View file

@ -59,28 +59,29 @@ export const previewInventoryMetricThresholdAlert = async ({
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
);
let numberOfTimesFired = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
const allConditionsFiredInMappedBucket = results.every((result) => {
const shouldFire = result[item].shouldFire as boolean[];
return shouldFire[mappedBucketIndex];
});
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;

View file

@ -72,7 +72,9 @@ export const evaluateAlert = (
typeof point.value === 'number' && comparisonFunction(point.value, threshold)
)
: [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),
};
});

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
// 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 nextState = isError

View file

@ -36,7 +36,7 @@ export const previewMetricThresholdAlert: (
params: PreviewMetricThresholdAlertParams,
iterations?: number,
precalculatedNumberOfGroups?: number
) => Promise<Array<number | null>> = async (
) => Promise<number[][]> = async (
{
callCluster,
params,
@ -77,15 +77,6 @@ export const previewMetricThresholdAlert: (
const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
const previewResults = await Promise.all(
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
// 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
@ -95,14 +86,25 @@ export const previewMetricThresholdAlert: (
numberOfResultBuckets / alertResultsPerExecution
);
let numberOfTimesFired = 0;
let numberOfNoDataResults = 0;
let numberOfErrors = 0;
for (let i = 0; i < numberOfExecutionBuckets; i++) {
const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
const allConditionsFiredInMappedBucket = alertResults.every(
(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 (someConditionsNoDataInMappedBucket) numberOfNoDataResults++;
if (someConditionsErrorInMappedBucket) numberOfErrors++;
}
return numberOfTimesFired;
return [numberOfTimesFired, numberOfNoDataResults, numberOfErrors];
})
);
return previewResults;
@ -152,9 +154,9 @@ export const previewMetricThresholdAlert: (
// so filter these results out entirely and only regard the resultA portion
.filter((value) => typeof value !== 'undefined')
.reduce((a, b) => {
if (typeof a !== 'number') return a;
if (typeof b !== 'number') return b;
return a + b;
if (!a) return b;
if (!b) return a;
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
})
);
return zippedResult as any;

View file

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