diff --git a/x-pack/plugins/infra/public/alerting/common/components/threshold.tsx b/x-pack/plugins/infra/public/alerting/common/components/threshold.tsx index 4df28abdfeb7c..922ea14c94e98 100644 --- a/x-pack/plugins/infra/public/alerting/common/components/threshold.tsx +++ b/x-pack/plugins/infra/public/alerting/common/components/threshold.tsx @@ -19,7 +19,7 @@ export interface ChartProps { export interface Props { chartProps: ChartProps; - comparator: Comparator; + comparator: Comparator | string; id: string; threshold: number; title: string; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/logs_history_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/logs_history_chart.tsx index 5857bdaba8b48..6d07b303f0ec7 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/logs_history_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/components/logs_history_chart.tsx @@ -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'; @@ -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; - alert: TopAlert>; -}) => { +const LogsHistoryChart = ({ rule }: { rule: Rule }) => { // Show the Logs History Chart ONLY if we have one criteria // So always pull the first criteria const criteria = rule.params.criteria[0]; @@ -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, @@ -84,7 +79,7 @@ const LogsHistoryChart = ({ -

{alertsHistory?.totalTriggeredAlerts || '-'}

+

{totalTriggeredAlerts || '-'}

@@ -102,10 +97,10 @@ const LogsHistoryChart = ({

- {alertsHistory?.avgTimeToRecoverUS + {avgTimeToRecoverUS ? convertTo({ unit: 'minutes', - microseconds: alertsHistory?.avgTimeToRecoverUS, + microseconds: avgTimeToRecoverUS, extended: true, }).formatted : '-'} diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx index 01fb690be4017..c0ca89ef05b88 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx @@ -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(''); + 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 ` 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, 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 - - {rule.params.criteria.map((criteria, idx) => { - const chartCriterion = criteria as PartialCriterion; - return ( - - ]} - filterSeriesByGroupName={[selectedSeries]} - /> + !!rule.params.criteria ? ( + + {rule.params.criteria.map((criteria, idx) => { + const chartCriterion = criteria as PartialCriterion; + return ( + + + {chartCriterion.comparator && ( + + +

+ {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, + }, + })} +

+
+
+ )} +
+ + + + + {chartCriterion.comparator && ( + + )} + + + , + , + ]} + filterSeriesByGroupName={[selectedSeries]} + /> + + +
+ ); + })} + {rule && rule.params.criteria.length === 1 && ( + + - ); - })} - {/* For now we show the history chart only if we have one criteria */} - {rule.params.criteria.length === 1 && ( - - - - )} -
+ )} +
+ ) : null ); }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts index 61a0859670549..c76778aa81251 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts @@ -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; alert: TopAlert>; + setAlertSummaryFields: React.Dispatch>; } diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index e098afb83d72a..ad393900f99db 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -334,31 +334,33 @@ const CriterionPreviewChart: React.FC = ({ -
- {groupByLabel != null ? ( - - - - ) : ( - - - - )} -
+ {!executionTimeRange && ( +
+ {groupByLabel != null ? ( + + + + ) : ( + + + + )} +
+ )} ); }; diff --git a/x-pack/plugins/infra/public/hooks/use_alerts_history.ts b/x-pack/plugins/infra/public/hooks/use_alerts_history.ts index 29a304da4b6fe..d97e3e0bea692 100644 --- a/x-pack/plugins/infra/public/hooks/use_alerts_history.ts +++ b/x-pack/plugins/infra/public/hooks/use_alerts_history.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { AsApiContract } from '@kbn/actions-plugin/common'; -import { HttpSetup } from '@kbn/core/public'; +import { useEffect, useRef } from 'react'; +import type { HttpSetup } from '@kbn/core/public'; import { ALERT_DURATION, ALERT_RULE_UUID, @@ -18,6 +17,9 @@ import { } from '@kbn/rule-data-utils'; import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { estypes } from '@elastic/elasticsearch'; +import { InfraClientCoreStart } from '../types'; interface Props { featureIds: ValidFeatureId[]; @@ -29,95 +31,62 @@ interface Props { } interface FetchAlertsHistory { totalTriggeredAlerts: number; - histogramTriggeredAlerts: Array<{ - key_as_string: string; - key: number; - doc_count: number; - }>; + histogramTriggeredAlerts: estypes.AggregationsDateHistogramBucketKeys[]; error?: string; avgTimeToRecoverUS: number; } -interface AlertsHistory { - isLoadingAlertsHistory: boolean; - errorAlertHistory?: string; - alertsHistory?: FetchAlertsHistory; -} export function useAlertsHistory({ featureIds, ruleId, dateRange }: Props) { - const { http } = useKibana().services; - const [triggeredAlertsHistory, setTriggeredAlertsHistory] = useState({ - isLoadingAlertsHistory: true, - }); - const isCancelledRef = useRef(false); + const { http } = useKibana().services; const abortCtrlRef = useRef(new AbortController()); - const loadRuleAlertsAgg = useCallback(async () => { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - - try { - if (!http) throw new Error('No http client'); - if (!featureIds || !featureIds.length) throw new Error('No featureIds'); - const { totalTriggeredAlerts, histogramTriggeredAlerts, error, avgTimeToRecoverUS } = - await fetchTriggeredAlertsHistory({ - featureIds, - http, - ruleId, - signal: abortCtrlRef.current.signal, - dateRange, - }); + const [state, refetch] = useAsyncFn( + () => { + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + return fetchTriggeredAlertsHistory({ + featureIds, + http, + ruleId, + dateRange, + signal: abortCtrlRef.current.signal, + }); + }, + [ruleId], + { loading: true } + ); - if (error) throw error; - if (!isCancelledRef.current) { - setTriggeredAlertsHistory((oldState: AlertsHistory) => ({ - ...oldState, - alertsHistory: { - totalTriggeredAlerts, - histogramTriggeredAlerts, - avgTimeToRecoverUS, - }, - isLoadingAlertsHistory: false, - })); - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - setTriggeredAlertsHistory((oldState: AlertsHistory) => ({ - ...oldState, - isLoadingAlertsHistory: false, - errorAlertHistory: error, - alertsHistory: undefined, - })); - } - } - } - }, [dateRange, featureIds, http, ruleId]); useEffect(() => { - loadRuleAlertsAgg(); - }, [loadRuleAlertsAgg]); + refetch(); + }, [refetch]); - return triggeredAlertsHistory; + const { value, error, loading } = state; + return { + ...value, + error, + loading, + refetch, + }; } -export async function fetchTriggeredAlertsHistory({ +async function fetchTriggeredAlertsHistory({ featureIds, http, ruleId, - signal, dateRange, + signal, }: { featureIds: ValidFeatureId[]; http: HttpSetup; ruleId: string; - signal: AbortSignal; dateRange: { from: string; to: string; }; + signal: AbortSignal; }): Promise { - try { - const res = await http.post>(`${BASE_RAC_ALERTS_API_PATH}/find`, { + return http + .post>>(`${BASE_RAC_ALERTS_API_PATH}/find`, { signal, body: JSON.stringify({ size: 0, @@ -165,22 +134,25 @@ export async function fetchTriggeredAlertsHistory({ }, }, }), - }); - const totalTriggeredAlerts = res?.hits.total.value; - const histogramTriggeredAlerts = res?.aggregations?.histogramTriggeredAlerts.buckets; - const avgTimeToRecoverUS = res?.aggregations?.avgTimeToRecoverUS.recoveryTime.value; - - return { - totalTriggeredAlerts, - histogramTriggeredAlerts, - avgTimeToRecoverUS, - }; - } catch (error) { - return { - error, - totalTriggeredAlerts: 0, - histogramTriggeredAlerts: [], - avgTimeToRecoverUS: 0, - }; - } + }) + .then(extractAlertsHistory); } + +const extractAlertsHistory = (response: estypes.SearchResponse>) => { + const totalTriggeredAlerts = (response.hits.total as estypes.SearchTotalHits).value || 0; + + const histogramAgg = response?.aggregations + ?.histogramTriggeredAlerts as estypes.AggregationsMultiBucketAggregateBase; + const histogramTriggeredAlerts = + histogramAgg.buckets as unknown as estypes.AggregationsDateHistogramBucketKeys[]; + + const avgTimeToRecoverAgg = response?.aggregations + ?.avgTimeToRecoverUS as estypes.AggregationsAvgAggregate; + const avgTimeToRecoverUS = avgTimeToRecoverAgg.value || 0; + + return { + totalTriggeredAlerts, + histogramTriggeredAlerts, + avgTimeToRecoverUS, + }; +}; diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 568a34ec452c9..55fefdbe21e54 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -60,7 +60,6 @@ "@kbn/shared-ux-router", "@kbn/shared-ux-link-redirect-app", "@kbn/observability-alert-details", - "@kbn/actions-plugin", "@kbn/ui-theme" ], "exclude": ["target/**/*"]