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 preview feature for metric threshold alerts #67684

Merged
merged 29 commits into from
Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
046a043
Add alert preview backend for threshold
Zacqary May 27, 2020
afa14e7
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary May 27, 2020
fb59bce
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary May 28, 2020
d06ed28
Get frontend and backend working
Zacqary May 28, 2020
5a35520
i18n fix
Zacqary May 28, 2020
a09e7f5
Add hook for inventory preview
Zacqary May 29, 2020
59e9bcd
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary Jun 1, 2020
83516a2
Merge branch 'master' into 65830-alert-preview
elasticmachine Jun 1, 2020
8dd5dd9
Remove breaking Boom import
Zacqary Jun 1, 2020
48861b1
Merge branch '65830-alert-preview' of github.com:Zacqary/kibana into …
Zacqary Jun 1, 2020
31c2708
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary Jun 4, 2020
e7406ef
Fix module import
Zacqary Jun 4, 2020
2e908fc
Move types to common folder, rename metric query file
Zacqary Jun 5, 2020
73400ea
Add too many buckets error r handler
Zacqary Jun 5, 2020
0e4732f
Switch to evaluating based on the alert interval
Zacqary Jun 5, 2020
925d01d
Add interval prompt to not enough data message
Zacqary Jun 5, 2020
7e3a20a
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary Jun 8, 2020
a04ddde
Fix type check
Zacqary Jun 8, 2020
6896892
Add recursive handling of too many buckets
Zacqary Jun 8, 2020
b804e5c
Update comment
Zacqary Jun 8, 2020
db10b8d
Avoid re-fetching groups every iteration
Zacqary Jun 8, 2020
2993450
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary Jun 8, 2020
2bbe889
Fix i18n
Zacqary Jun 9, 2020
84385f1
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary Jun 9, 2020
04b5eee
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary Jun 10, 2020
d3b8bab
Merge branch 'master' into 65830-alert-preview
elasticmachine Jun 11, 2020
a22e6b8
Clean up interpolation and apply useCallbacks
Zacqary Jun 15, 2020
1de9ca6
Merge remote-tracking branch 'upstream/master' into 65830-alert-preview
Zacqary Jun 15, 2020
8dd4219
Merge branch '65830-alert-preview' of github.com:Zacqary/kibana into …
Zacqary Jun 15, 2020
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 @@ -4,25 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { debounce } from 'lodash';
import { debounce, pick } from 'lodash';
import * as rt from 'io-ts';
import { HttpSetup } from 'src/core/public';
import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react';
import {
EuiSpacer,
EuiText,
EuiFormRow,
EuiButton,
EuiButtonEmpty,
EuiCheckbox,
EuiToolTip,
EuiIcon,
EuiFieldSearch,
EuiSelect,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
Comparator,
Aggregators,
METRIC_THRESHOLD_ALERT_TYPE_ID,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../server/lib/alerting/metric_threshold/types';
import {
INFRA_ALERT_PREVIEW_PATH,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move these types to the common directory. That will have the added bonus of allowing us to remove // eslint-disable-next-line @kbn/eslint/no-restricted-paths

alertPreviewRequestParamsRT,
alertPreviewSuccessResponsePayloadRT,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../server/routes/alerting/preview';
import {
ForLastExpression,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
Expand All @@ -40,6 +53,7 @@ import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
import { ExpressionRow } from './expression_row';
import { AlertContextMeta, TimeUnit, MetricExpression } from '../types';
import { ExpressionChart } from './expression_chart';
import { validateMetricThreshold } from './validation';

const FILTER_TYPING_DEBOUNCE_MS = 500;

Expand All @@ -66,6 +80,22 @@ const defaultExpression = {
timeUnit: 'm',
} as MetricExpression;

async function getAlertPreview({
fetch,
params,
}: {
fetch: HttpSetup['fetch'];
params: rt.TypeOf<typeof alertPreviewRequestParamsRT>;
}): Promise<rt.TypeOf<typeof alertPreviewSuccessResponsePayloadRT>> {
return await fetch(`${INFRA_ALERT_PREVIEW_PATH}`, {
method: 'POST',
body: JSON.stringify({
...params,
alertType: METRIC_THRESHOLD_ALERT_TYPE_ID,
}),
});
}

export const Expressions: React.FC<Props> = (props) => {
const { setAlertParams, alertParams, errors, alertsContext } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
Expand All @@ -75,6 +105,13 @@ export const Expressions: React.FC<Props> = (props) => {
toastWarning: alertsContext.toastNotifications.addWarning,
});

const [previewLookbackInterval, setPreviewLookbackInterval] = useState<string>('h');
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(false);
const [previewError, setPreviewError] = useState<boolean>(false);
const [previewResult, setPreviewResult] = useState<rt.TypeOf<
typeof alertPreviewSuccessResponsePayloadRT
> | null>(null);

const [timeSize, setTimeSize] = useState<number | undefined>(1);
const [timeUnit, setTimeUnit] = useState<TimeUnit>('m');
const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
Expand Down Expand Up @@ -240,6 +277,13 @@ export const Expressions: React.FC<Props> = (props) => {
[onFilterChange]
);

const isPreviewDisabled = useMemo(() => {
const validationResult = validateMetricThreshold({ criteria: alertParams.criteria } as any);
return Object.values(validationResult.errors).some((result) =>
Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
);
}, [alertParams.criteria]);

return (
<>
<EuiSpacer size={'m'} />
Expand Down Expand Up @@ -374,6 +418,168 @@ export const Expressions: React.FC<Props> = (props) => {
}}
/>
</EuiFormRow>

<EuiSpacer size={'m'} />
<EuiFormRow
label={i18n.translate('xpack.infra.metrics.alertFlyout.previewLabel', {
defaultMessage: 'Preview',
})}
fullWidth
compressed
>
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiSelect
id="selectPreviewLookbackInterval"
value={previewLookbackInterval}
onChange={(e) => {
setPreviewLookbackInterval(e.target.value);
setPreviewResult(null);
}}
options={previewOptions}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isLoading={isPreviewLoading}
isDisabled={isPreviewDisabled}
onClick={async () => {
setIsPreviewLoading(true);
setPreviewResult(null);
setPreviewError(false);
try {
const result = await getAlertPreview({
fetch: alertsContext.http.fetch,
params: {
...pick(alertParams, 'criteria', 'groupBy', 'filterQuery'),
sourceId: alertParams.sourceId,
lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
},
});
setPreviewResult(result);
} catch (e) {
setPreviewError(true);
} finally {
setIsPreviewLoading(false);
}
}}
>
{i18n.translate('xpack.infra.metrics.alertFlyout.testAlertTrigger', {
defaultMessage: 'Test alert trigger',
})}
</EuiButton>
</EuiFlexItem>
<EuiSpacer size={'s'} />
</EuiFlexGroup>
{previewResult && (
<>
<EuiSpacer size={'s'} />
<EuiText>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewResult"
defaultMessage="This alert would have fired {fired} {timeOrTimes} in the past {lookback}"
values={{
timeOrTimes:
previewResult.resultTotals.fired === 1 ? firedTimeLabel : firedTimesLabel,
fired: <strong>{previewResult.resultTotals.fired}</strong>,
lookback: previewOptions.find((e) => e.value === previewLookbackInterval)
?.shortText,
}}
/>{' '}
{alertParams.groupBy ? (
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewGroups"
defaultMessage="across {numberOfGroups} {groupName}{plural}."
values={{
numberOfGroups: <strong>{previewResult.numberOfGroups}</strong>,
groupName: alertParams.groupBy,
plural: previewResult.numberOfGroups !== 1 ? 's' : '',
}}
/>
) : (
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewAllData"
defaultMessage="across the entire infrastructure."
/>
)}
</EuiText>
{alertParams.alertOnNoData && previewResult.resultTotals.noData && (
<>
<EuiSpacer size={'s'} />
<EuiText>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult"
defaultMessage="There were {noData} results of no data."
values={{
noData: <strong>{previewResult.resultTotals.noData}</strong>,
}}
/>
</EuiText>
</>
)}
</>
)}
{previewError && (
<>
<EuiSpacer size={'s'} />
<EuiText>
<FormattedMessage
id="xpack.infra.metrics.alertFlyout.alertPreviewError"
defaultMessage="An error occurred when trying to preview this alert trigger."
/>
</EuiText>
</>
)}
</>
</EuiFormRow>
<EuiSpacer size={'m'} />
</>
);
};

const previewOptions = [
{
value: 'h',
text: i18n.translate('xpack.infra.metrics.alertFlyout.lastHourLabel', {
defaultMessage: 'Last hour',
}),
shortText: i18n.translate('xpack.infra.metrics.alertFlyout.hourLabel', {
defaultMessage: 'hour',
}),
},
{
value: 'd',
text: i18n.translate('xpack.infra.metrics.alertFlyout.lastDayLabel', {
defaultMessage: 'Last day',
}),
shortText: i18n.translate('xpack.infra.metrics.alertFlyout.dayLabel', {
defaultMessage: 'day',
}),
},
{
value: 'w',
text: i18n.translate('xpack.infra.metrics.alertFlyout.lastWeekLabel', {
defaultMessage: 'Last week',
}),
shortText: i18n.translate('xpack.infra.metrics.alertFlyout.weekLabel', {
defaultMessage: 'week',
}),
},
{
value: 'M',
text: i18n.translate('xpack.infra.metrics.alertFlyout.lastMonthLabel', {
defaultMessage: 'Last month',
}),
shortText: i18n.translate('xpack.infra.metrics.alertFlyout.monthLabel', {
defaultMessage: 'month',
}),
},
];

const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
defaultMessage: 'time',
});
const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
defaultMessage: 'times',
});
2 changes: 2 additions & 0 deletions x-pack/plugins/infra/server/infra_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { initInventoryMetaRoute } from './routes/inventory_metadata';
import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources';
import { initSourceRoute } from './routes/source';
import { initAlertPreviewRoute } from './routes/alerting';

export const initInfraServer = (libs: InfraBackendLibs) => {
const schema = makeExecutableSchema({
Expand Down Expand Up @@ -64,4 +65,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initInventoryMetaRoute(libs);
initLogSourceConfigurationRoutes(libs);
initLogSourceStatusRoutes(libs);
initAlertPreviewRoute(libs);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Aggregators } from './types';
import { Aggregators } from '../types';
export const createPercentileAggregation = (
type: Aggregators.P95 | Aggregators.P99,
field: string
Expand Down
Loading