Skip to content

Commit

Permalink
Add alert grouping functionality to the observability alerts page (#1…
Browse files Browse the repository at this point in the history
…89958)

Closes #190995

## Summary

This PR adds grouping functionality to the alerts page alert table based
on @umbopepato's implementation in this [draft
PR](#183114) (basically, he
implemented the feature and I adjusted a bit for our use case :D).

For now, we only added the **rule** and **source** as default grouping,
and I will create a ticket to add tags as well. The challenge with tags
is that since it is an array, the value of the alert is joined by a
comma as the group, which does not match with what we want for tags.


![image](https:/user-attachments/assets/c08c3cb1-4c6c-4918-8071-3c5913de41f6)

Here is how we show the rules that don't have a group by field selected
for them: (We used "ungrouped" similar to what we have in SLOs)


![image](https:/user-attachments/assets/280bbd34-6c3b-41c1-803b-dcc6448f6fb4)

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: DeDe Morton <[email protected]>
Co-authored-by: Shahzad <[email protected]>
  • Loading branch information
4 people authored Aug 22, 2024
1 parent 00975ad commit 34d392b
Show file tree
Hide file tree
Showing 27 changed files with 605 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('AlertsGrouping', () => {
},
{
range: {
'@timestamp': {
'kibana.alert.time_range': {
gte: mockDate.from,
lte: mockDate.to,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const AlertsGroupingInternal = <T extends BaseAlertsGroupAggregations>(
};

return (
<AlertsGroupingLevel
<AlertsGroupingLevel<T>
{...props}
getGrouping={getGrouping}
groupingLevel={level}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { type GroupingAggregation } from '@kbn/grouping';
import { isNoneGroup } from '@kbn/grouping';
import type { DynamicGroupingProps } from '@kbn/grouping/src';
import { parseGroupingQuery } from '@kbn/grouping/src';
import { ALERT_TIME_RANGE } from '@kbn/rule-data-utils';
import {
useGetAlertsGroupAggregationsQuery,
UseGetAlertsGroupAggregationsQueryProps,
Expand Down Expand Up @@ -94,7 +95,7 @@ export const AlertsGroupingLevel = typedMemo(
...filters,
{
range: {
'@timestamp': {
[ALERT_TIME_RANGE]: {
gte: from,
lte: to,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export const useAlertsGroupingState = (groupingId: string) => {
setGroupingState((prevState) => ({
...prevState,
[groupingId]: {
// @ts-expect-error options might not be defined
options: [],
// @ts-expect-error activeGroups might not be defined
activeGroups: initialActiveGroups,
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-alerts-grouping/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ReactElement } from 'react';

export interface GroupModel {
activeGroups: string[];
options: Array<{ key: string; label: string }>;
options?: Array<{ key: string; label: string }>;
}

export interface AlertsGroupingState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ALERT_STATUS_RECOVERED,
ALERT_STATUS_UNTRACKED,
} from '@kbn/rule-data-utils';
import { Filter } from '@kbn/es-query';
import { ALERT_STATUS_ALL } from './constants';

export type Maybe<T> = T | null | undefined;
Expand Down Expand Up @@ -39,6 +40,7 @@ export type AlertStatus =
export interface AlertStatusFilter {
status: AlertStatus;
query: string;
filter: Filter[];
label: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const DEFAULT_QUERY_STRING = '';
export const ALL_ALERTS: AlertStatusFilter = {
status: ALERT_STATUS_ALL,
query: '',
filter: [],
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.showAll', {
defaultMessage: 'Show all',
}),
Expand All @@ -30,6 +31,16 @@ export const ALL_ALERTS: AlertStatusFilter = {
export const ACTIVE_ALERTS: AlertStatusFilter = {
status: ALERT_STATUS_ACTIVE,
query: `${ALERT_STATUS}: "${ALERT_STATUS_ACTIVE}"`,
filter: [
{
query: {
match_phrase: {
[ALERT_STATUS]: ALERT_STATUS_ACTIVE,
},
},
meta: {},
},
],
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.active', {
defaultMessage: 'Active',
}),
Expand All @@ -38,6 +49,16 @@ export const ACTIVE_ALERTS: AlertStatusFilter = {
export const RECOVERED_ALERTS: AlertStatusFilter = {
status: ALERT_STATUS_RECOVERED,
query: `${ALERT_STATUS}: "${ALERT_STATUS_RECOVERED}"`,
filter: [
{
query: {
match_phrase: {
[ALERT_STATUS]: ALERT_STATUS_RECOVERED,
},
},
meta: {},
},
],
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.recovered', {
defaultMessage: 'Recovered',
}),
Expand All @@ -46,6 +67,16 @@ export const RECOVERED_ALERTS: AlertStatusFilter = {
export const UNTRACKED_ALERTS: AlertStatusFilter = {
status: ALERT_STATUS_UNTRACKED,
query: `${ALERT_STATUS}: "${ALERT_STATUS_UNTRACKED}"`,
filter: [
{
query: {
match_phrase: {
[ALERT_STATUS]: ALERT_STATUS_UNTRACKED,
},
},
meta: {},
},
],
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.untracked', {
defaultMessage: 'Untracked',
}),
Expand All @@ -56,3 +87,9 @@ export const ALERT_STATUS_QUERY = {
[RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query,
[UNTRACKED_ALERTS.status]: UNTRACKED_ALERTS.query,
};

export const ALERT_STATUS_FILTER = {
[ACTIVE_ALERTS.status]: ACTIVE_ALERTS.filter,
[RECOVERED_ALERTS.status]: RECOVERED_ALERTS.filter,
[UNTRACKED_ALERTS.status]: UNTRACKED_ALERTS.filter,
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,29 @@ import {
AlertsTableConfigurationRegistry,
RenderCustomActionsRowArgs,
} from '@kbn/triggers-actions-ui-plugin/public/types';
import { casesFeatureId, observabilityFeatureId } from '../../../../common';
import { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types';
import { HttpSetup } from '@kbn/core-http-browser';
import { NotificationsStart } from '@kbn/core-notifications-browser';
import {
casesFeatureId,
observabilityAlertFeatureIds,
observabilityFeatureId,
} from '../../../../common';
import { AlertActions } from '../../../pages/alerts/components/alert_actions';
import { useGetAlertFlyoutComponents } from '../../alerts_flyout/use_get_alert_flyout_components';
import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry';
import { ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID } from '../../../constants';
import type { ConfigSchema } from '../../../plugin';
import { getRenderCellValue } from '../common/render_cell_value';
import { getColumns } from '../common/get_columns';
import { getPersistentControlsHook } from './get_persistent_controls';

export const getAlertsPageTableConfiguration = (
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry,
config: ConfigSchema
config: ConfigSchema,
dataViews: DataViewsServicePublic,
http: HttpSetup,
notifications: NotificationsStart
): AlertsTableConfigurationRegistry => {
const renderCustomActionsRow = (props: RenderCustomActionsRowArgs) => {
return (
Expand All @@ -34,7 +46,7 @@ export const getAlertsPageTableConfiguration = (
);
};
return {
id: observabilityFeatureId,
id: ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID,
cases: { featureId: casesFeatureId, owner: [observabilityFeatureId] },
columns: getColumns({ showRuleName: true }),
getRenderCellValue,
Expand All @@ -53,6 +65,15 @@ export const getAlertsPageTableConfiguration = (
return { header, body, footer };
},
ruleTypeIds: observabilityRuleTypeRegistry.list(),
usePersistentControls: getPersistentControlsHook({
groupingId: ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID,
featureIds: observabilityAlertFeatureIds,
services: {
dataViews,
http,
notifications,
},
}),
showInspectButton: true,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useMemo, useCallback } from 'react';
import { type AlertsGroupingProps, useAlertsGroupingState } from '@kbn/alerts-grouping';
import { useAlertsDataView } from '@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view';
import { useGetGroupSelectorStateless } from '@kbn/grouping/src/hooks/use_get_group_selector';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { AlertsByGroupingAgg } from '../types';

interface GetPersistentControlsParams {
groupingId: string;
featureIds: AlertConsumers[];
maxGroupingLevels?: number;
services: Pick<
AlertsGroupingProps<AlertsByGroupingAgg>['services'],
'dataViews' | 'http' | 'notifications'
>;
}

export const getPersistentControlsHook =
({
groupingId,
featureIds,
maxGroupingLevels = 3,
services: { dataViews, http, notifications },
}: GetPersistentControlsParams) =>
() => {
const { grouping, updateGrouping } = useAlertsGroupingState(groupingId);

const onGroupChange = useCallback(
(selectedGroups: string[]) => {
updateGrouping({
activeGroups:
grouping.activeGroups?.filter((g) => g !== 'none').concat(selectedGroups) ?? [],
});
},
[grouping, updateGrouping]
);

const { dataView } = useAlertsDataView({
featureIds,
dataViewsService: dataViews,
http,
toasts: notifications.toasts,
});

const groupSelector = useGetGroupSelectorStateless({
groupingId,
onGroupChange,
fields: dataView?.fields ?? [],
defaultGroupingOptions:
grouping.options?.filter((option) => !grouping.activeGroups.includes(option.key)) ?? [],
maxGroupingLevels,
});

return useMemo(() => {
return {
right: groupSelector,
};
}, [groupSelector]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ALERT_START, AlertConsumers } from '@kbn/rule-data-utils';
import {
AlertsTableConfigurationRegistry,
RenderCustomActionsRowArgs,
} from '@kbn/triggers-actions-ui-plugin/public/types';
import { casesFeatureId, observabilityFeatureId } from '../../../../common';
import { AlertActions } from '../../../pages/alerts/components/alert_actions';
import { useGetAlertFlyoutComponents } from '../../alerts_flyout/use_get_alert_flyout_components';
import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry';
import type { ConfigSchema } from '../../../plugin';
import { getRenderCellValue } from '../common/render_cell_value';
import { getColumns } from '../common/get_columns';

export const getObservabilityTableConfiguration = (
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry,
config: ConfigSchema
): AlertsTableConfigurationRegistry => {
const renderCustomActionsRow = (props: RenderCustomActionsRowArgs) => {
return (
<AlertActions
{...props}
config={config}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
/>
);
};
return {
id: AlertConsumers.OBSERVABILITY,
cases: { featureId: casesFeatureId, owner: [observabilityFeatureId] },
columns: getColumns({ showRuleName: true }),
getRenderCellValue,
sort: [
{
[ALERT_START]: {
order: 'desc' as SortOrder,
},
},
],
useActionsColumn: () => ({
renderCustomActionsRow,
}),
useInternalFlyout: () => {
const { header, body, footer } = useGetAlertFlyoutComponents(observabilityRuleTypeRegistry);
return { header, body, footer };
},
ruleTypeIds: observabilityRuleTypeRegistry.list(),
showInspectButton: true,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,39 @@
*/

import { AlertTableConfigRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/alert_table_config_registry';
import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types';
import { HttpSetup } from '@kbn/core-http-browser';
import { NotificationsStart } from '@kbn/core-notifications-browser';
import type { ConfigSchema } from '../../plugin';
import { ObservabilityRuleTypeRegistry } from '../..';
import { getAlertsPageTableConfiguration } from './alerts/get_alerts_page_table_configuration';
import { getRuleDetailsTableConfiguration } from './rule_details/get_rule_details_table_configuration';
import { getSloAlertsTableConfiguration } from './slo/get_slo_alerts_table_configuration';
import { getObservabilityTableConfiguration } from './observability/get_alerts_page_table_configuration';

export const registerAlertsTableConfiguration = (
alertTableConfigRegistry: AlertTableConfigRegistry,
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry,
config: ConfigSchema
config: ConfigSchema,
dataViews: DataViewsServicePublic,
http: HttpSetup,
notifications: NotificationsStart
) => {
// Alert page
const alertsPageAlertsTableConfig = getAlertsPageTableConfiguration(
// Observability table
const observabilityAlertsTableConfig = getObservabilityTableConfiguration(
observabilityRuleTypeRegistry,
config
);
alertTableConfigRegistry.register(observabilityAlertsTableConfig);

// Alerts page
const alertsPageAlertsTableConfig = getAlertsPageTableConfiguration(
observabilityRuleTypeRegistry,
config,
dataViews,
http,
notifications
);
alertTableConfigRegistry.register(alertsPageAlertsTableConfig);

// Rule details page
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export interface BucketItem {
key: string;
doc_count: number;
}

export interface AlertsByGroupingAgg extends Record<string, unknown> {
groupByFields: {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: BucketItem[];
};
ruleTags: {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: BucketItem[];
};
rulesCountAggregation?: {
value: number;
};
sourceCountAggregation?: {
value: number;
};
groupsCount: {
value: number;
};
unitsCount: {
value: number;
};
}
Loading

0 comments on commit 34d392b

Please sign in to comment.