Skip to content

Commit

Permalink
[Alert details page][Custom threshold] Add history chart to custom th…
Browse files Browse the repository at this point in the history
…reshold alert details page (elastic#176513)

Closes elastic#175200

## Summary

This PR adds a history chart to the custom threshold alert details page.
The history chart is only added if we have only 1 condition in the rule.

Also, this PR fixes the issue of not applying group by information on
the main chart that I mistakenly introduced during refactoring code in
this [PR](elastic#175777).


![image](https://github.com/elastic/kibana/assets/12370520/22b449c4-71de-4714-8ab8-9fdd244eb943)

## 🧪 How to test
- Create a custom threshold rule with only one condition
- Go to the alert details page from the alert table actions
- You should be able to see the history chart for the last 30 days with
the correct filtering both for optional KQL and group by information
  • Loading branch information
maryam-saeidi authored Feb 13, 2024
1 parent 873ae31 commit 1aa5e38
Show file tree
Hide file tree
Showing 13 changed files with 475 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,61 @@ describe('useAlertsHistory', () => {
expect(result.current.data.histogramTriggeredAlerts?.length).toEqual(31);
expect(result.current.data.totalTriggeredAlerts).toEqual(32);
});

it('calls http post including term queries', async () => {
const controller = new AbortController();
const signal = controller.signal;
const mockedHttpPost = jest.fn();
const http = {
post: mockedHttpPost.mockResolvedValue({
hits: { total: { value: 32, relation: 'eq' }, max_score: null, hits: [] },
aggregations: {
avgTimeToRecoverUS: { doc_count: 28, recoveryTime: { value: 134959464.2857143 } },
histogramTriggeredAlerts: {
buckets: [
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 0 },
],
},
},
}),
} as unknown as HttpSetup;

const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
() =>
useAlertsHistory({
http,
featureIds: [AlertConsumers.APM],
ruleId,
dateRange: { from: start, to: end },
queries: [
{
term: {
'kibana.alert.group.value': {
value: 'host=1',
},
},
},
],
}),
{
wrapper,
}
);

await act(async () => {
await waitFor(() => result.current.isSuccess);
});
expect(mockedHttpPost).toBeCalledWith('/internal/rac/alerts/find', {
body:
'{"size":0,"feature_ids":["apm"],"query":{"bool":{"must":[' +
'{"term":{"kibana.alert.rule.uuid":"cfd36e60-ef22-11ed-91eb-b7893acacfe2"}},' +
'{"term":{"kibana.alert.group.value":{"value":"host=1"}}},' +
'{"range":{"kibana.alert.time_range":{"from":"2023-04-10T00:00:00.000Z","to":"2023-05-10T00:00:00.000Z"}}}]}},' +
'"aggs":{"histogramTriggeredAlerts":{"date_histogram":{"field":"kibana.alert.start","fixed_interval":"1d",' +
'"extended_bounds":{"min":"2023-04-10T00:00:00.000Z","max":"2023-05-10T00:00:00.000Z"}}},' +
'"avgTimeToRecoverUS":{"filter":{"term":{"kibana.alert.status":"recovered"}},' +
'"aggs":{"recoveryTime":{"avg":{"field":"kibana.alert.duration.us"}}}}}}',
signal,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
* 2.0.
*/

import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { type HttpSetup } from '@kbn/core/public';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
ALERT_DURATION,
ALERT_RULE_UUID,
Expand All @@ -26,6 +27,7 @@ export interface Props {
from: string;
to: string;
};
queries?: QueryDslQueryContainer[];
}

interface FetchAlertsHistory {
Expand All @@ -45,7 +47,13 @@ export const EMPTY_ALERTS_HISTORY = {
histogramTriggeredAlerts: [] as AggregationsDateHistogramBucketKeys[],
avgTimeToRecoverUS: 0,
};
export function useAlertsHistory({ featureIds, ruleId, dateRange, http }: Props): UseAlertsHistory {
export function useAlertsHistory({
featureIds,
ruleId,
dateRange,
http,
queries,
}: Props): UseAlertsHistory {
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: ['useAlertsHistory'],
queryFn: async ({ signal }) => {
Expand All @@ -58,6 +66,7 @@ export function useAlertsHistory({ featureIds, ruleId, dateRange, http }: Props)
ruleId,
dateRange,
signal,
queries,
});
},
refetchOnWindowFocus: false,
Expand Down Expand Up @@ -87,12 +96,14 @@ interface AggsESResponse {
};
};
}

export async function fetchTriggeredAlertsHistory({
featureIds,
http,
ruleId,
dateRange,
signal,
queries = [],
}: {
featureIds: ValidFeatureId[];
http: HttpSetup;
Expand All @@ -102,6 +113,7 @@ export async function fetchTriggeredAlertsHistory({
to: string;
};
signal?: AbortSignal;
queries?: QueryDslQueryContainer[];
}): Promise<FetchAlertsHistory> {
try {
const responseES = await http.post<AggsESResponse>(`${BASE_RAC_ALERTS_API_PATH}/find`, {
Expand All @@ -117,6 +129,7 @@ export async function fetchTriggeredAlertsHistory({
[ALERT_RULE_UUID]: ruleId,
},
},
...queries,
{
range: {
[ALERT_TIME_RANGE]: dateRange,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
} from '../../mocks/custom_threshold_rule';
import { CustomThresholdAlertFields } from '../../types';
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
import AlertDetailsAppSection, { CustomThresholdAlert } from './alert_details_app_section';
import { CustomThresholdAlert } from '../types';
import AlertDetailsAppSection from './alert_details_app_section';
import { Groups } from './groups';
import { Tags } from './tags';

Expand All @@ -28,6 +29,17 @@ const mockedChartStartContract = chartPluginMock.createStartContract();
jest.mock('@kbn/observability-alert-details', () => ({
AlertAnnotation: () => {},
AlertActiveTimeRangeAnnotation: () => {},
useAlertsHistory: () => ({
data: {
histogramTriggeredAlerts: [
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 2 },
],
avgTimeToRecoverUS: 0,
totalTriggeredAlerts: 2,
},
isLoading: false,
isError: false,
}),
}));

jest.mock('@kbn/observability-get-padded-alert-time-range-util', () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import chroma from 'chroma-js';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useEffect, useState } from 'react';
Expand All @@ -20,7 +21,7 @@ import {
useEuiTheme,
transparentize,
} from '@elastic/eui';
import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common';
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import {
ALERT_END,
Expand All @@ -30,34 +31,27 @@ import {
TAGS,
} from '@kbn/rule-data-utils';
import { DataView } from '@kbn/data-views-plugin/common';
import chroma from 'chroma-js';
import type {
EventAnnotationConfig,
PointInTimeEventAnnotationConfig,
RangeEventAnnotationConfig,
} from '@kbn/event-annotation-common';
import moment from 'moment';
import { AlertHistoryChart } from './alert_history';
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,
CustomThresholdAlertFields,
CustomThresholdRuleTypeParams,
MetricExpression,
} from '../../types';
import { AlertSummaryField } from '../../../..';
import { AlertParams, MetricExpression } from '../../types';
import { TIME_LABELS } from '../criterion_preview_chart/criterion_preview_chart';
import { Threshold } from '../custom_threshold';
import { getGroupFilters } from '../helpers/get_group';
import { CustomThresholdRule, CustomThresholdAlert } from '../types';
import { LogRateAnalysis } from './log_rate_analysis';
import { Groups } from './groups';
import { Tags } from './tags';
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';

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

interface AppSectionProps {
alert: CustomThresholdAlert;
rule: CustomThresholdRule;
Expand Down Expand Up @@ -261,14 +255,18 @@ export default function AlertDetailsAppSection({
</EuiFlexItem>
<EuiFlexItem grow={5}>
<RuleConditionChart
metricExpression={criterion}
additionalFilters={getGroupFilters(groups)}
annotations={annotations}
chartOptions={{
// For alert details page, the series type needs to be changed to 'bar_stacked'
// due to https://github.com/elastic/elastic-charts/issues/2323
seriesType: 'bar_stacked',
}}
dataView={dataView}
searchConfiguration={ruleParams.searchConfiguration}
groupBy={ruleParams.groupBy}
annotations={annotations}
metricExpression={criterion}
searchConfiguration={ruleParams.searchConfiguration}
timeRange={timeRange}
// For alert details page, the series type needs to be changed to 'bar_stacked' due to https://github.com/elastic/elastic-charts/issues/2323
seriesType={'bar_stacked'}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand All @@ -278,6 +276,7 @@ export default function AlertDetailsAppSection({
{hasLogRateAnalysisLicense && (
<LogRateAnalysis alert={alert} dataView={dataView} rule={rule} services={services} />
)}
<AlertHistoryChart alert={alert} dataView={dataView} rule={rule} />
</EuiFlexGroup>
) : null;

Expand Down
Loading

0 comments on commit 1aa5e38

Please sign in to comment.