Skip to content

Commit

Permalink
[AO] - Add rule-specific fields, threshold, and reuse alert details p…
Browse files Browse the repository at this point in the history
…ackage components (#154569)

## Summary
It fixes #153675, fixes #153567,
fixes #153946, fixes #154845

<img width="1603" alt="Screenshot 2023-04-11 at 17 40 38"
src="https://user-images.githubusercontent.com/6838659/231215968-63b9ab6d-3b82-47c6-ace9-347306283b2a.png">

---------
  • Loading branch information
fkanout authored Apr 13, 2023
1 parent fe9985b commit 89e8d03
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 195 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface ChartProps {

export interface Props {
chartProps: ChartProps;
comparator: Comparator;
comparator: Comparator | string;
id: string;
threshold: number;
title: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React from 'react';
import { Rule } from '@kbn/alerting-plugin/common';
import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { convertTo, TopAlert } from '@kbn/observability-plugin/public';
import { convertTo } from '@kbn/observability-plugin/public';
import { AnnotationDomainType, LineAnnotation, Position } from '@elastic/charts';
import { EuiIcon, EuiBadge } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
Expand All @@ -20,13 +20,7 @@ import { type PartialCriterion } from '../../../../../../common/alerting/logs/lo
import { CriterionPreview } from '../../expression_editor/criterion_preview_chart';
import { PartialRuleParams } from '../../../../../../common/alerting/logs/log_threshold';

const LogsHistoryChart = ({
rule,
alert,
}: {
rule: Rule<PartialRuleParams>;
alert: TopAlert<Record<string, any>>;
}) => {
const LogsHistoryChart = ({ rule }: { rule: Rule<PartialRuleParams> }) => {
// Show the Logs History Chart ONLY if we have one criteria
// So always pull the first criteria
const criteria = rule.params.criteria[0];
Expand All @@ -40,14 +34,15 @@ const LogsHistoryChart = ({
lte: DateMath.parse(dateRange.to, { roundUp: true })!.valueOf(),
};

const { alertsHistory } = useAlertsHistory({
const { histogramTriggeredAlerts, avgTimeToRecoverUS, totalTriggeredAlerts } = useAlertsHistory({
featureIds: [AlertConsumers.LOGS],
ruleId: rule.id,
dateRange,
});

const alertHistoryAnnotations =
alertsHistory?.histogramTriggeredAlerts
.filter((annotation) => annotation.doc_count > 0)
histogramTriggeredAlerts
?.filter((annotation) => annotation.doc_count > 0)
.map((annotation) => {
return {
dataValue: annotation.key,
Expand Down Expand Up @@ -84,7 +79,7 @@ const LogsHistoryChart = ({
<EuiFlexItem grow={false}>
<EuiText color="danger">
<EuiTitle size="s">
<h3>{alertsHistory?.totalTriggeredAlerts || '-'}</h3>
<h3>{totalTriggeredAlerts || '-'}</h3>
</EuiTitle>
</EuiText>
</EuiFlexItem>
Expand All @@ -102,10 +97,10 @@ const LogsHistoryChart = ({
<EuiText>
<EuiTitle size="s">
<h3>
{alertsHistory?.avgTimeToRecoverUS
{avgTimeToRecoverUS
? convertTo({
unit: 'minutes',
microseconds: alertsHistory?.avgTimeToRecoverUS,
microseconds: avgTimeToRecoverUS,
extended: true,
}).formatted
: '-'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,87 +4,163 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { ALERT_DURATION, ALERT_END } from '@kbn/rule-data-utils';
import compact from 'lodash/compact';
import React, { useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { LIGHT_THEME } from '@elastic/charts';
import { EuiPanel } from '@elastic/eui';
import { ALERT_END, ALERT_EVALUATION_VALUE, ALERT_START } from '@kbn/rule-data-utils';
import moment from 'moment';
import React from 'react';
import { useTheme } from '@emotion/react';
import { EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
AlertAnnotation,
getPaddedAlertTimeRange,
AlertActiveTimeRangeAnnotation,
} from '@kbn/observability-alert-details';
import { useEuiTheme } from '@elastic/eui';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { getChartGroupNames } from '../../../../../common/utils/get_chart_group_names';
import { type PartialCriterion } from '../../../../../common/alerting/logs/log_threshold';
import {
ComparatorToi18nMap,
ComparatorToi18nSymbolsMap,
type PartialCriterion,
} from '../../../../../common/alerting/logs/log_threshold';
import { CriterionPreview } from '../expression_editor/criterion_preview_chart';
import { AlertAnnotation } from './components/alert_annotation';
import { AlertDetailsAppSectionProps } from './types';
import { Threshold } from '../../../common/components/threshold';

const LogsHistoryChart = React.lazy(() => import('./components/logs_history_chart'));
const formatThreshold = (threshold: number) => String(threshold);

const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) => {
const ruleWindowSizeMS = moment
.duration(rule.params.timeSize, rule.params.timeUnit)
.asMilliseconds();
const alertDurationMS = alert.fields[ALERT_DURATION]! / 1000;
const TWENTY_TIMES_RULE_WINDOW_MS = 20 * ruleWindowSizeMS;
const AlertDetailsAppSection = ({
rule,
alert,
setAlertSummaryFields,
}: AlertDetailsAppSectionProps) => {
const [selectedSeries, setSelectedSeries] = useState<string>('');
const { uiSettings } = useKibanaContextForPlugin().services;
const { euiTheme } = useEuiTheme();
const theme = useTheme();
const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]);
const alertEnd = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]).valueOf() : undefined;

/**
* The `CriterionPreview` chart shows all the series/data stacked when there is a GroupBy in the rule parameters.
* e.g., `host.name`, the chart will show stacks of data by hostname.
* We only need the chart to show the series that is related to the selected alert.
* The chart series are built based on the GroupBy in the rule params
* Each series have an id which is the just a joining of fields value of the GroupBy `getChartGroupNames`
* We filter down the series using this group name
*/
const alertFieldsFromGroupBy = compact(
rule.params.groupBy?.map((fieldNameGroupBy) => {
const field = Object.keys(alert.fields).find(
(alertFiledName) => alertFiledName === fieldNameGroupBy
);
if (field) return alert.fields[field];
})
);
const selectedSeries = getChartGroupNames(alertFieldsFromGroupBy);

/**
* This is part or the requirements (RFC).
* If the alert is less than 20 units of `FOR THE LAST <x> <units>` then we should draw a time range of 20 units.
* IE. The user set "FOR THE LAST 5 minutes" at a minimum we should show 100 minutes.
*/
const rangeFrom =
alertDurationMS < TWENTY_TIMES_RULE_WINDOW_MS
? Number(moment(alert.start).subtract(TWENTY_TIMES_RULE_WINDOW_MS, 'millisecond').format('x'))
: Number(moment(alert.start).subtract(ruleWindowSizeMS, 'millisecond').format('x'));
useEffect(() => {
/**
* The `CriterionPreview` chart shows all the series/data stacked when there is a GroupBy in the rule parameters.
* e.g., `host.name`, the chart will show stacks of data by hostname.
* We only need the chart to show the series that is related to the selected alert.
* The chart series are built based on the GroupBy in the rule params
* Each series have an id which is the just a joining of fields value of the GroupBy `getChartGroupNames`
* We filter down the series using this group name
*/
const alertFieldsFromGroupBy =
rule.params.groupBy?.reduce(
(selectedFields: Record<string, any>, field) => ({
...selectedFields,
...{ [field]: alert.fields[field] },
}),
{}
) || {};

const rangeTo = alert.active
? Date.now()
: Number(moment(alert.fields[ALERT_END]).add(ruleWindowSizeMS, 'millisecond').format('x'));
setSelectedSeries(getChartGroupNames(Object.values(alertFieldsFromGroupBy)));
const alertSummaryFields = Object.entries(alertFieldsFromGroupBy).map(([label, value]) => ({
label,
value,
}));
setAlertSummaryFields(alertSummaryFields);
}, [alert.fields, rule.params.groupBy, setAlertSummaryFields]);

return (
// Create a chart per-criteria
<EuiFlexGroup direction="column">
{rule.params.criteria.map((criteria, idx) => {
const chartCriterion = criteria as PartialCriterion;
return (
<EuiFlexItem key={`${chartCriterion.field}${idx}`}>
<CriterionPreview
ruleParams={rule.params}
logViewReference={{
type: 'log-view-reference',
logViewId: rule.params.logView.logViewId,
}}
chartCriterion={chartCriterion}
showThreshold={true}
executionTimeRange={{ gte: rangeFrom, lte: rangeTo }}
annotations={[<AlertAnnotation alertStarted={alert.start} />]}
filterSeriesByGroupName={[selectedSeries]}
/>
!!rule.params.criteria ? (
<EuiFlexGroup direction="column" data-test-subj="logsThresholdAlertDetailsPage">
{rule.params.criteria.map((criteria, idx) => {
const chartCriterion = criteria as PartialCriterion;
return (
<EuiPanel
key={`${chartCriterion.field}${idx}`}
hasBorder={true}
data-test-subj="logsHistoryChartAlertDetails"
>
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
{chartCriterion.comparator && (
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.infra.logs.alertDetails.chart.chartTitle', {
defaultMessage: 'Logs for {field} {comparator} {value}',
values: {
field: chartCriterion.field,
comparator: ComparatorToi18nMap[chartCriterion.comparator],
value: chartCriterion.value,
},
})}
</h2>
</EuiTitle>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem style={{ maxHeight: 120 }} grow={1}>
<EuiSpacer size="s" />
{chartCriterion.comparator && (
<Threshold
title={`Threshold breached`}
chartProps={{ theme, baseTheme: LIGHT_THEME }}
comparator={ComparatorToi18nSymbolsMap[rule.params.count.comparator]}
id={`${chartCriterion.field}-${chartCriterion.value}`}
threshold={rule.params.count.value}
value={Number(alert.fields[ALERT_EVALUATION_VALUE])}
valueFormatter={formatThreshold}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={5}>
<CriterionPreview
ruleParams={rule.params}
logViewReference={{
type: 'log-view-reference',
logViewId: rule.params.logView.logViewId,
}}
chartCriterion={chartCriterion}
showThreshold={true}
executionTimeRange={{
gte: Number(moment(timeRange.from).format('x')),
lte: Number(moment(timeRange.to).format('x')),
}}
annotations={[
<AlertAnnotation
key={`${alert.start}${chartCriterion.field}${idx}-start-alert-annotation`}
id={`${alert.start}${chartCriterion.field}${idx}-start-alert-annotation`}
alertStart={alert.start}
color={euiTheme.colors.danger}
dateFormat={uiSettings.get(UI_SETTINGS.DATE_FORMAT)}
/>,
<AlertActiveTimeRangeAnnotation
key={`${alert.start}${chartCriterion.field}${idx}-active-alert-annotation`}
id={`${alert.start}${chartCriterion.field}${idx}-active-alert-annotation`}
alertStart={alert.start}
alertEnd={alertEnd}
color={euiTheme.colors.danger}
/>,
]}
filterSeriesByGroupName={[selectedSeries]}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
})}
{rule && rule.params.criteria.length === 1 && (
<EuiFlexItem>
<LogsHistoryChart rule={rule} />
</EuiFlexItem>
);
})}
{/* For now we show the history chart only if we have one criteria */}
{rule.params.criteria.length === 1 && (
<EuiFlexItem>
<LogsHistoryChart alert={alert} rule={rule} />
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiFlexGroup>
) : null
);
};
// eslint-disable-next-line import/no-default-export
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
*/

import { Rule } from '@kbn/alerting-plugin/common';
import { TopAlert } from '@kbn/observability-plugin/public';
import { AlertSummaryField, TopAlert } from '@kbn/observability-plugin/public';
import { PartialRuleParams } from '../../../../../common/alerting/logs/log_threshold';

export interface AlertDetailsAppSectionProps {
rule: Rule<PartialRuleParams>;
alert: TopAlert<Record<string, any>>;
setAlertSummaryFields: React.Dispatch<React.SetStateAction<AlertSummaryField[] | undefined>>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -334,31 +334,33 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
<Settings tooltip={tooltipProps} theme={getChartTheme(isDarkMode)} />
</Chart>
</ChartContainer>
<div style={{ textAlign: 'center' }}>
{groupByLabel != null ? (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.logs.alerts.dataTimeRangeLabelWithGrouping"
defaultMessage="Last {lookback} {timeLabel} of data, grouped by {groupByLabel} (showing {displayedGroups}/{totalGroups} groups)"
values={{
groupByLabel,
timeLabel,
lookback,
displayedGroups: filteredSeries.length,
totalGroups: series.length,
}}
/>
</EuiText>
) : (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.logs.alerts.dataTimeRangeLabel"
defaultMessage="Last {lookback} {timeLabel} of data"
values={{ timeLabel, lookback }}
/>
</EuiText>
)}
</div>
{!executionTimeRange && (
<div style={{ textAlign: 'center' }}>
{groupByLabel != null ? (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.logs.alerts.dataTimeRangeLabelWithGrouping"
defaultMessage="Last {lookback} {timeLabel} of data, grouped by {groupByLabel} (showing {displayedGroups}/{totalGroups} groups)"
values={{
groupByLabel,
timeLabel,
lookback,
displayedGroups: filteredSeries.length,
totalGroups: series.length,
}}
/>
</EuiText>
) : (
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.infra.logs.alerts.dataTimeRangeLabel"
defaultMessage="Last {lookback} {timeLabel} of data"
values={{ timeLabel, lookback }}
/>
</EuiText>
)}
</div>
)}
</>
);
};
Loading

0 comments on commit 89e8d03

Please sign in to comment.