[Metrics UI] Fix previewing of No Data results (#73753)
This commit is contained in:
parent
5e86d2f848
commit
c2d8869cca
|
@ -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"
|
||||||
|
|
|
@ -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'} />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue