Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Metrics UI] Add checkbox to optionally drop partial buckets from threshold alerts #107676

Merged
merged 1 commit into from
Aug 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
EuiToolTip,
EuiIcon,
EuiFieldSearch,
EuiAccordion,
EuiPanel,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
Expand Down Expand Up @@ -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'} />
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ export interface AlertParams {
sourceId: string;
filterQueryText?: string;
alertOnNoData?: boolean;
shouldDropPartialBuckets?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand All @@ -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(
Expand All @@ -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;
Expand Down Expand Up @@ -105,15 +107,17 @@ 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,
index,
timefield,
groupBy,
filterQuery,
timeframe
timeframe,
shouldDropPartialBuckets
) {
const { aggType, timeSize, timeUnit } = params;
const hasGroupBy = groupBy && groupBy.length;
Expand Down Expand Up @@ -145,6 +149,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 = (
Expand All @@ -166,11 +180,7 @@ const getMetric: (
...result,
[Object.values(bucket.key)
.map((value) => value)
.join(', ')]: getValuesFromAggregations(bucket, aggType, {
from,
to,
bucketSizeInMillis: intervalAsMS,
}),
.join(', ')]: getValuesFromAggregations(bucket, aggType, dropPartialBucketsOptions),
}),
{}
);
Expand All @@ -184,7 +194,7 @@ const getMetric: (
[UNGROUPED_FACTORY_KEY]: getValuesFromAggregations(
(result.aggregations! as unknown) as Aggregation,
aggType,
{ from, to, bucketSizeInMillis: intervalAsMS }
dropPartialBucketsOptions
),
};
} catch (e) {
Expand Down Expand Up @@ -224,47 +234,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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface PreviewMetricThresholdAlertParams {
criteria: MetricExpressionParams[];
groupBy: string | undefined | string[];
filterQuery: string | undefined;
shouldDropPartialBuckets?: boolean;
};
config: InfraSource['configuration'];
lookback: Unit;
Expand Down