Skip to content

Commit

Permalink
[Alert details page][Custom threshold] Add group by and tags fields t…
Browse files Browse the repository at this point in the history
…o the alert details page summary (elastic#174254)

Closes elastic#155698
Closes elastic#174236

## Summary

This PR adds group and tag information to the alert details page summary
fields for the custom threshold rule:


![image](https://github.com/elastic/kibana/assets/12370520/d93e32bd-99ae-414f-87ed-5ca5884acfa6)


## 🧪 How to test
- Enable alert details page config
    ```
    xpack.observability.unsafe.alertDetails.observability.enabled: true
    ```
- Create a custom threshold rule with a group by and add more than 3
rule tags
- After the alert is fired, go to the alert details page (from alert
flyout or menu action in the alert table); you should see the group by
and tags information there.
  • Loading branch information
maryam-saeidi authored and CoenWarmer committed Feb 15, 2024
1 parent 602e8ad commit 81a5d0c
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ import { EuiLink } from '@elastic/eui';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { coreMock as mockCoreMock } from '@kbn/core/public/mocks';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
buildCustomThresholdAlert,
buildCustomThresholdRule,
} from '../../mocks/custom_threshold_rule';
import AlertDetailsAppSection from './alert_details_app_section';
import { CustomThresholdAlertFields } from '../../types';
import { ExpressionChart } from '../expression_chart';
import AlertDetailsAppSection, { CustomThresholdAlert } from './alert_details_app_section';
import { Groups } from './groups';
import { Tags } from './tags';

const mockedChartStartContract = chartPluginMock.createStartContract();

Expand Down Expand Up @@ -57,12 +61,15 @@ describe('AlertDetailsAppSection', () => {
const mockedSetAlertSummaryFields = jest.fn();
const ruleLink = 'ruleLink';

const renderComponent = () => {
const renderComponent = (
alert: Partial<CustomThresholdAlert> = {},
alertFields: Partial<ParsedTechnicalFields & CustomThresholdAlertFields> = {}
) => {
return render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<AlertDetailsAppSection
alert={buildCustomThresholdAlert()}
alert={buildCustomThresholdAlert(alert, alertFields)}
rule={buildCustomThresholdRule()}
ruleLink={ruleLink}
setAlertSummaryFields={mockedSetAlertSummaryFields}
Expand All @@ -83,9 +90,43 @@ describe('AlertDetailsAppSection', () => {
expect(result.getByTestId('thresholdRule-2000-2500')).toBeTruthy();
});

it('should render rule link', async () => {
it('should render alert summary fields', async () => {
renderComponent();

expect(mockedSetAlertSummaryFields).toBeCalledTimes(1);
expect(mockedSetAlertSummaryFields).toBeCalledWith([
{
label: 'Source',
value: (
<Groups
groups={[
{
field: 'host.name',
value: 'host-1',
},
]}
/>
),
},
{
label: 'Tags',
value: <Tags tags={['tag 1', 'tag 2']} />,
},
{
label: 'Rule',
value: (
<EuiLink data-test-subj="thresholdRuleAlertDetailsAppSectionRuleLink" href={ruleLink}>
Monitoring hosts
</EuiLink>
),
},
]);
});

it('should not render group and tag summary fields', async () => {
const alertFields = { tags: [], 'kibana.alert.group': undefined };
renderComponent({}, alertFields);

expect(mockedSetAlertSummaryFields).toBeCalledTimes(1);
expect(mockedSetAlertSummaryFields).toBeCalledWith([
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,37 @@ import {
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES } from '@kbn/rule-data-utils';
import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common';
import { AlertAnnotation, AlertActiveTimeRangeAnnotation } from '@kbn/observability-alert-details';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import {
ALERT_END,
ALERT_START,
ALERT_EVALUATION_VALUES,
ALERT_GROUP,
TAGS,
} from '@kbn/rule-data-utils';
import { DataView } from '@kbn/data-views-plugin/common';
import { MetricsExplorerChartType } from '../../../../../common/custom_threshold_rule/types';
import { useLicense } from '../../../../hooks/use_license';
import { useKibana } from '../../../../utils/kibana_react';
import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter';
import { AlertSummaryField, TopAlert } from '../../../..';
import { AlertParams, CustomThresholdRuleTypeParams } from '../../types';
import {
AlertParams,
CustomThresholdAlertFields,
CustomThresholdRuleTypeParams,
} from '../../types';
import { ExpressionChart } from '../expression_chart';
import { TIME_LABELS } from '../criterion_preview_chart/criterion_preview_chart';
import { Threshold } from '../custom_threshold';
import { LogRateAnalysis } from './log_rate_analysis';
import { Groups } from './groups';
import { Tags } from './tags';

// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
export type CustomThresholdRule = Rule<CustomThresholdRuleTypeParams>;
export type CustomThresholdAlert = TopAlert;
export type CustomThresholdAlert = TopAlert<CustomThresholdAlertFields>;

const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm';
const ALERT_START_ANNOTATION_ID = 'alert_start_annotation';
Expand Down Expand Up @@ -89,21 +101,46 @@ export default function AlertDetailsAppSection({
];

useEffect(() => {
setAlertSummaryFields([
{
const groups = alert.fields[ALERT_GROUP];
const tags = alert.fields[TAGS];
const alertSummaryFields = [];
if (groups) {
alertSummaryFields.push({
label: i18n.translate(
'xpack.observability.customThreshold.rule.alertDetailsAppSection.summaryField.rule',
'xpack.observability.customThreshold.rule.alertDetailsAppSection.summaryField.source',
{
defaultMessage: 'Rule',
defaultMessage: 'Source',
}
),
value: (
<EuiLink data-test-subj="thresholdRuleAlertDetailsAppSectionRuleLink" href={ruleLink}>
{rule.name}
</EuiLink>
value: <Groups groups={groups} />,
});
}
if (tags && tags.length > 0) {
alertSummaryFields.push({
label: i18n.translate(
'xpack.observability.customThreshold.rule.alertDetailsAppSection.summaryField.tags',
{
defaultMessage: 'Tags',
}
),
},
]);
value: <Tags tags={tags} />,
});
}
alertSummaryFields.push({
label: i18n.translate(
'xpack.observability.customThreshold.rule.alertDetailsAppSection.summaryField.rule',
{
defaultMessage: 'Rule',
}
),
value: (
<EuiLink data-test-subj="thresholdRuleAlertDetailsAppSectionRuleLink" href={ruleLink}>
{rule.name}
</EuiLink>
),
});

setAlertSummaryFields(alertSummaryFields);
}, [alert, rule, ruleLink, setAlertSummaryFields]);

const derivedIndexPattern = useMemo<DataViewBase>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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';

export function Groups({ groups }: { groups: Array<{ field: string; value: string }> }) {
return (
<>
{groups &&
groups.map((group) => {
return (
<span key={group.field}>
{group.field}: <strong>{group.value}</strong>
<br />
</span>
);
})}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { EuiBadge, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';

export function Tags({ tags }: { tags: string[] }) {
const [isMoreTagsOpen, setIsMoreTagsOpen] = useState(false);
const onMoreTagsClick = () => setIsMoreTagsOpen((isPopoverOpen) => !isPopoverOpen);
const closePopover = () => setIsMoreTagsOpen(false);
const moreTags = tags.length > 3 && (
<EuiBadge
key="more"
onClick={onMoreTagsClick}
onClickAriaLabel={i18n.translate(
'xpack.observability.customThreshold.rule.alertDetailsAppSection.summaryField.moreTags.ariaLabel',
{
defaultMessage: 'more tags badge',
}
)}
>
<FormattedMessage
id="xpack.observability.customThreshold.rule.alertDetailsAppSection.summaryField.moreTags"
defaultMessage="+{number} more"
values={{ number: tags.length - 3 }}
/>
</EuiBadge>
);

return (
<>
{tags.slice(0, 3).map((tag) => (
<EuiBadge key={tag}>{tag}</EuiBadge>
))}
<br />
<EuiPopover button={moreTags} isOpen={isMoreTagsOpen} closePopover={closePopover}>
{tags.slice(3).map((tag) => (
<EuiBadge key={tag}>{tag}</EuiBadge>
))}
</EuiPopover>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*/

import { v4 as uuidv4 } from 'uuid';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import { CustomThresholdAlertFields } from '../types';
import { Aggregators, Comparator } from '../../../../common/custom_threshold_rule/types';

import {
Expand Down Expand Up @@ -145,7 +147,8 @@ export const buildCustomThresholdRule = (
};

export const buildCustomThresholdAlert = (
alert: Partial<CustomThresholdAlert> = {}
alert: Partial<CustomThresholdAlert> = {},
alertFields: Partial<ParsedTechnicalFields & CustomThresholdAlertFields> = {}
): CustomThresholdAlert => {
return {
link: '/app/metrics/explorer',
Expand Down Expand Up @@ -187,6 +190,7 @@ export const buildCustomThresholdAlert = (
alertOnGroupDisappear: true,
},
'kibana.alert.evaluation.values': [2500, 5],
'kibana.alert.group': [{ field: 'host.name', value: 'host-1' }],
'kibana.alert.rule.category': 'Custom threshold (Beta)',
'kibana.alert.rule.consumer': 'alerts',
'kibana.alert.rule.execution.uuid': '62dd07ef-ead9-4b1f-a415-7c83d03925f7',
Expand All @@ -199,7 +203,7 @@ export const buildCustomThresholdAlert = (
'@timestamp': '2023-03-28T14:40:00.000Z',
'kibana.alert.reason': 'system.cpu.user.pct reported no data in the last 1m for ',
'kibana.alert.action_group': 'custom_threshold.nodata',
tags: [],
tags: ['tag 1', 'tag 2'],
'kibana.alert.duration.us': 248391946000,
'kibana.alert.time_range': {
gte: '2023-03-13T14:06:23.695Z',
Expand All @@ -214,6 +218,7 @@ export const buildCustomThresholdAlert = (
'kibana.version': '8.8.0',
'kibana.alert.flapping': false,
'kibana.alert.rule.revision': 1,
...alertFields,
},
active: true,
start: 1678716383695,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
import { OsqueryPluginStart } from '@kbn/osquery-plugin/public';
import { ALERT_GROUP } from '@kbn/rule-data-utils';
import { SharePluginStart } from '@kbn/share-plugin/public';
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import {
Expand Down Expand Up @@ -93,6 +94,9 @@ export interface CustomThresholdRuleTypeParams extends RuleTypeParams {
searchConfiguration: SerializedSearchSourceFields;
groupBy?: string | string[];
}
export interface CustomThresholdAlertFields {
[ALERT_GROUP]?: Array<{ field: string; value: string }>;
}

export const expressionTimestampsRT = rt.type({
fromTimestamp: rt.number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { ReactNode } from 'react';
import { EuiText, EuiFlexItem, EuiFlexGrid, useIsWithinBreakpoints } from '@elastic/eui';
import { EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui';

export interface AlertSummaryField {
label: ReactNode | string;
Expand All @@ -16,23 +17,16 @@ interface AlertSummaryProps {
}

export function AlertSummary({ alertSummaryFields }: AlertSummaryProps) {
const isMobile = useIsWithinBreakpoints(['xs', 's']);
return (
<EuiFlexGrid
responsive={false}
data-test-subj="alert-summary-container"
style={{
gridTemplateColumns: isMobile ? 'repeat(2, 1fr)' : 'repeat(5, 1fr)',
}}
>
<EuiFlexGroup data-test-subj="alert-summary-container" gutterSize="xl">
{alertSummaryFields?.map((field, idx) => {
return (
<EuiFlexItem key={idx}>
<EuiFlexItem key={idx} grow={false}>
<EuiText color="subdued">{field.label}</EuiText>
<EuiText>{field.value}</EuiText>
</EuiFlexItem>
);
})}
</EuiFlexGrid>
</EuiFlexGroup>
);
}

0 comments on commit 81a5d0c

Please sign in to comment.