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

[Alert details page][Custom threshold] Add group by and tags fields to the alert details page summary #174254

Merged
merged 11 commits into from
Jan 17, 2024
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>
);
}