[Metrics UI] Add checkbox to optionally drop partial buckets (#107676)

This commit is contained in:
Zacqary Adam Xeper 2021-08-05 16:23:33 -05:00 committed by GitHub
parent e9913264c7
commit eed9723c85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 54 deletions

View file

@ -17,6 +17,8 @@ import {
EuiToolTip,
EuiIcon,
EuiFieldSearch,
EuiAccordion,
EuiPanel,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -259,6 +261,11 @@ export const Expressions: React.FC<Props> = (props) => {
return alertParams.groupBy;
}, [alertParams.groupBy]);
const areAllAggsRate = useMemo(
() => alertParams.criteria?.every((c) => c.aggType === Aggregators.RATE),
[alertParams.criteria]
);
return (
<>
<EuiSpacer size={'m'} />
@ -323,27 +330,60 @@ export const Expressions: React.FC<Props> = (props) => {
</div>
<EuiSpacer size={'m'} />
<EuiCheckbox
id="metrics-alert-no-data-toggle"
label={
<>
{i18n.translate('xpack.infra.metrics.alertFlyout.alertOnNoData', {
defaultMessage: "Alert me if there's no data",
})}{' '}
<EuiToolTip
content={i18n.translate('xpack.infra.metrics.alertFlyout.noDataHelpText', {
defaultMessage:
'Enable this to trigger the action if the metric(s) do not report any data over the expected time period, or if the alert fails to query Elasticsearch',
})}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</>
}
checked={alertParams.alertOnNoData}
onChange={(e) => setAlertParams('alertOnNoData', e.target.checked)}
/>
<EuiAccordion
id="advanced-options-accordion"
buttonContent={i18n.translate('xpack.infra.metrics.alertFlyout.advancedOptions', {
defaultMessage: 'Advanced options',
})}
>
<EuiPanel color="subdued">
<EuiCheckbox
id="metrics-alert-no-data-toggle"
label={
<>
{i18n.translate('xpack.infra.metrics.alertFlyout.alertOnNoData', {
defaultMessage: "Alert me if there's no data",
})}{' '}
<EuiToolTip
content={i18n.translate('xpack.infra.metrics.alertFlyout.noDataHelpText', {
defaultMessage:
'Enable this to trigger the action if the metric(s) do not report any data over the expected time period, or if the alert fails to query Elasticsearch',
})}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</>
}
checked={alertParams.alertOnNoData}
onChange={(e) => setAlertParams('alertOnNoData', e.target.checked)}
/>
<EuiCheckbox
id="metrics-alert-partial-buckets-toggle"
label={
<>
{i18n.translate('xpack.infra.metrics.alertFlyout.shouldDropPartialBuckets', {
defaultMessage: 'Drop partial buckets when evaluating data',
})}{' '}
<EuiToolTip
content={i18n.translate(
'xpack.infra.metrics.alertFlyout.dropPartialBucketsHelpText',
{
defaultMessage:
"Enable this to drop the most recent bucket of evaluation data if it's less than {timeSize}{timeUnit}.",
values: { timeSize, timeUnit },
}
)}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</>
}
checked={areAllAggsRate || alertParams.shouldDropPartialBuckets}
disabled={areAllAggsRate}
onChange={(e) => setAlertParams('shouldDropPartialBuckets', e.target.checked)}
/>
</EuiPanel>
</EuiAccordion>
<EuiSpacer size={'m'} />
<EuiFormRow
@ -400,7 +440,14 @@ export const Expressions: React.FC<Props> = (props) => {
alertThrottle={alertThrottle}
alertNotifyWhen={alertNotifyWhen}
alertType={METRIC_THRESHOLD_ALERT_TYPE_ID}
alertParams={pick(alertParams, 'criteria', 'groupBy', 'filterQuery', 'sourceId')}
alertParams={pick(
alertParams,
'criteria',
'groupBy',
'filterQuery',
'sourceId',
'shouldDropPartialBuckets'
)}
showNoDataResults={alertParams.alertOnNoData}
validate={validateMetricThreshold}
groupByDisplayName={groupByPreviewDisplayName}

View file

@ -61,4 +61,5 @@ export interface AlertParams {
sourceId: string;
filterQueryText?: string;
alertOnNoData?: boolean;
shouldDropPartialBuckets?: boolean;
}

View file

@ -45,6 +45,7 @@ export interface EvaluatedAlertParams {
criteria: MetricExpressionParams[];
groupBy: string | undefined | string[];
filterQuery: string | undefined;
shouldDropPartialBuckets?: boolean;
}
export const evaluateAlert = <Params extends EvaluatedAlertParams = EvaluatedAlertParams>(
@ -53,7 +54,7 @@ export const evaluateAlert = <Params extends EvaluatedAlertParams = EvaluatedAle
config: InfraSource['configuration'],
timeframe?: { start: number; end: number }
) => {
const { criteria, groupBy, filterQuery } = params;
const { criteria, groupBy, filterQuery, shouldDropPartialBuckets } = params;
return Promise.all(
criteria.map(async (criterion) => {
const currentValues = await getMetric(
@ -63,7 +64,8 @@ export const evaluateAlert = <Params extends EvaluatedAlertParams = EvaluatedAle
config.fields.timestamp,
groupBy,
filterQuery,
timeframe
timeframe,
shouldDropPartialBuckets
);
const { threshold, warningThreshold, comparator, warningComparator } = criterion;
@ -103,7 +105,8 @@ const getMetric: (
timefield: string,
groupBy: string | undefined | string[],
filterQuery: string | undefined,
timeframe?: { start: number; end: number }
timeframe?: { start: number; end: number },
shouldDropPartialBuckets?: boolean
) => Promise<Record<string, number[]>> = async function (
esClient,
params,
@ -111,7 +114,8 @@ const getMetric: (
timefield,
groupBy,
filterQuery,
timeframe
timeframe,
shouldDropPartialBuckets
) {
const { aggType, timeSize, timeUnit } = params;
const hasGroupBy = groupBy && groupBy.length;
@ -143,6 +147,16 @@ const getMetric: (
filterQuery
);
const dropPartialBucketsOptions =
// Rate aggs always drop partial buckets; guard against this boolean being passed as false
shouldDropPartialBuckets || aggType === Aggregators.RATE
? {
from,
to,
bucketSizeInMillis: intervalAsMS,
}
: null;
try {
if (hasGroupBy) {
const bucketSelector = (
@ -164,11 +178,7 @@ const getMetric: (
...result,
[Object.values(bucket.key)
.map((value) => value)
.join(', ')]: getValuesFromAggregations(bucket, aggType, {
from,
to,
bucketSizeInMillis: intervalAsMS,
}),
.join(', ')]: getValuesFromAggregations(bucket, aggType, dropPartialBucketsOptions),
}),
{}
);
@ -182,7 +192,7 @@ const getMetric: (
[UNGROUPED_FACTORY_KEY]: getValuesFromAggregations(
(result.aggregations! as unknown) as Aggregation,
aggType,
{ from, to, bucketSizeInMillis: intervalAsMS }
dropPartialBucketsOptions
),
};
} catch (e) {
@ -222,47 +232,46 @@ const dropPartialBuckets = ({ from, to, bucketSizeInMillis }: DropPartialBucketO
const getValuesFromAggregations = (
aggregations: Aggregation,
aggType: MetricExpressionParams['aggType'],
dropPartialBucketsOptions: DropPartialBucketOptions
dropPartialBucketsOptions: DropPartialBucketOptions | null
) => {
try {
const { buckets } = aggregations.aggregatedIntervals;
if (!buckets.length) return null; // No Data state
let mappedBuckets;
if (aggType === Aggregators.COUNT) {
return buckets.map((bucket) => ({
mappedBuckets = buckets.map((bucket) => ({
key: bucket.from_as_string,
value: bucket.doc_count,
}));
}
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
return buckets.map((bucket) => {
} else if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
mappedBuckets = buckets.map((bucket) => {
const values = bucket.aggregatedValue?.values || [];
const firstValue = first(values);
if (!firstValue) return null;
return { key: bucket.from_as_string, value: firstValue.value };
});
}
if (aggType === Aggregators.AVERAGE) {
return buckets.map((bucket) => ({
} else if (aggType === Aggregators.AVERAGE) {
mappedBuckets = buckets.map((bucket) => ({
key: bucket.key_as_string ?? bucket.from_as_string,
value: bucket.aggregatedValue?.value ?? null,
}));
} else if (aggType === Aggregators.RATE) {
mappedBuckets = buckets.map((bucket) => ({
key: bucket.key_as_string ?? bucket.from_as_string,
value: bucket.aggregatedValue?.value ?? null,
}));
} else {
mappedBuckets = buckets.map((bucket) => ({
key: bucket.key_as_string ?? bucket.from_as_string,
value: bucket.aggregatedValue?.value ?? null,
}));
}
if (aggType === Aggregators.RATE) {
return buckets
.map((bucket) => ({
key: bucket.key_as_string ?? bucket.from_as_string,
value: bucket.aggregatedValue?.value ?? null,
}))
.filter(dropPartialBuckets(dropPartialBucketsOptions));
if (dropPartialBucketsOptions) {
return mappedBuckets.filter(dropPartialBuckets(dropPartialBucketsOptions));
}
return buckets.map((bucket) => ({
key: bucket.key_as_string ?? bucket.from_as_string,
value: bucket.aggregatedValue?.value ?? null,
}));
return mappedBuckets;
} catch (e) {
return NaN; // Error state
}

View file

@ -26,6 +26,7 @@ interface PreviewMetricThresholdAlertParams {
criteria: MetricExpressionParams[];
groupBy: string | undefined | string[];
filterQuery: string | undefined;
shouldDropPartialBuckets?: boolean;
};
config: InfraSource['configuration'];
lookback: Unit;