Skip to content

Commit

Permalink
[AO] - Move the hook that fetches the alert annotations in the Alert …
Browse files Browse the repository at this point in the history
…Details page to Observability (#157178)

## Summary
Fixes #154170 by:
- Make the `use_alerts_history`
[hook](https://github.com/elastic/kibana/blob/main/x-pack/plugins/infra/public/hooks/use_alerts_history.ts)
sharable: move it to the Observability package and updates the imports in
`APM` and `Infra` to use it
- Provided tests
  • Loading branch information
fkanout authored May 16, 2023
1 parent 87f0c8e commit 24260d7
Show file tree
Hide file tree
Showing 10 changed files with 334 additions and 303 deletions.
1 change: 1 addition & 0 deletions x-pack/packages/observability/alert_details/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
export { AlertAnnotation } from './src/components/alert_annotation';
export { AlertActiveTimeRangeAnnotation } from './src/components/alert_active_time_range_annotation';
export { getPaddedAlertTimeRange } from './src/helpers/get_padded_alert_time_range';
export { useAlertsHistory } from './src/hooks/use_alerts_history';
2 changes: 1 addition & 1 deletion x-pack/packages/observability/alert_details/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

module.exports = {
preset: '@kbn/test/jest_node',
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/observability/alert_details'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* 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 { HttpSetup } from '@kbn/core-http-browser';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, renderHook } from '@testing-library/react-hooks';
import {
type UseAlertsHistory,
useAlertsHistory,
type Props as useAlertsHistoryProps,
} from './use_alerts_history';

const queryClient = new QueryClient({
logger: {
log: () => {},
warn: () => {},
error: () => {},
},
defaultOptions: {
queries: { retry: false, cacheTime: 0 },
},
});
const wrapper = ({ children }: any) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe('useAlertsHistory', () => {
const start = '2023-04-10T00:00:00.000Z';
const end = '2023-05-10T00:00:00.000Z';
const ruleId = 'cfd36e60-ef22-11ed-91eb-b7893acacfe2';

afterEach(() => {
jest.clearAllMocks();
});

it('returns no data with error when http client is not provided', async () => {
const http = undefined;
const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
() =>
useAlertsHistory({
http,
featureIds: [AlertConsumers.APM],
ruleId,
dateRange: { from: start, to: end },
}),
{
wrapper,
}
);
await act(async () => {
await waitFor(() => result.current.isError);
});
expect(result.current.isError).toBeTruthy();
expect(result.current.isSuccess).toBeFalsy();
expect(result.current.isLoading).toBeFalsy();
});
it('returns no data when API error', async () => {
const http = {
post: jest.fn().mockImplementation(() => {
throw new Error('ES error');
}),
} as unknown as HttpSetup;
const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
() =>
useAlertsHistory({
http,
featureIds: [AlertConsumers.APM],
ruleId,
dateRange: { from: start, to: end },
}),
{
wrapper,
}
);
await act(async () => {
await waitFor(() => result.current.isError);
});
expect(result.current.isError).toBeTruthy();
expect(result.current.isSuccess).toBeFalsy();
expect(result.current.isLoading).toBeFalsy();
});

it('returns the alert history chart data', async () => {
const http = {
post: jest.fn().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 },
{ key_as_string: '2023-04-11T00:00:00.000Z', key: 1681171200000, doc_count: 0 },
{ key_as_string: '2023-04-12T00:00:00.000Z', key: 1681257600000, doc_count: 0 },
{ key_as_string: '2023-04-13T00:00:00.000Z', key: 1681344000000, doc_count: 0 },
{ key_as_string: '2023-04-14T00:00:00.000Z', key: 1681430400000, doc_count: 0 },
{ key_as_string: '2023-04-15T00:00:00.000Z', key: 1681516800000, doc_count: 0 },
{ key_as_string: '2023-04-16T00:00:00.000Z', key: 1681603200000, doc_count: 0 },
{ key_as_string: '2023-04-17T00:00:00.000Z', key: 1681689600000, doc_count: 0 },
{ key_as_string: '2023-04-18T00:00:00.000Z', key: 1681776000000, doc_count: 0 },
{ key_as_string: '2023-04-19T00:00:00.000Z', key: 1681862400000, doc_count: 0 },
{ key_as_string: '2023-04-20T00:00:00.000Z', key: 1681948800000, doc_count: 0 },
{ key_as_string: '2023-04-21T00:00:00.000Z', key: 1682035200000, doc_count: 0 },
{ key_as_string: '2023-04-22T00:00:00.000Z', key: 1682121600000, doc_count: 0 },
{ key_as_string: '2023-04-23T00:00:00.000Z', key: 1682208000000, doc_count: 0 },
{ key_as_string: '2023-04-24T00:00:00.000Z', key: 1682294400000, doc_count: 0 },
{ key_as_string: '2023-04-25T00:00:00.000Z', key: 1682380800000, doc_count: 0 },
{ key_as_string: '2023-04-26T00:00:00.000Z', key: 1682467200000, doc_count: 0 },
{ key_as_string: '2023-04-27T00:00:00.000Z', key: 1682553600000, doc_count: 0 },
{ key_as_string: '2023-04-28T00:00:00.000Z', key: 1682640000000, doc_count: 0 },
{ key_as_string: '2023-04-29T00:00:00.000Z', key: 1682726400000, doc_count: 0 },
{ key_as_string: '2023-04-30T00:00:00.000Z', key: 1682812800000, doc_count: 0 },
{ key_as_string: '2023-05-01T00:00:00.000Z', key: 1682899200000, doc_count: 0 },
{ key_as_string: '2023-05-02T00:00:00.000Z', key: 1682985600000, doc_count: 0 },
{ key_as_string: '2023-05-03T00:00:00.000Z', key: 1683072000000, doc_count: 0 },
{ key_as_string: '2023-05-04T00:00:00.000Z', key: 1683158400000, doc_count: 0 },
{ key_as_string: '2023-05-05T00:00:00.000Z', key: 1683244800000, doc_count: 0 },
{ key_as_string: '2023-05-06T00:00:00.000Z', key: 1683331200000, doc_count: 0 },
{ key_as_string: '2023-05-07T00:00:00.000Z', key: 1683417600000, doc_count: 0 },
{ key_as_string: '2023-05-08T00:00:00.000Z', key: 1683504000000, doc_count: 0 },
{ key_as_string: '2023-05-09T00:00:00.000Z', key: 1683590400000, doc_count: 0 },
{ key_as_string: '2023-05-10T00:00:00.000Z', key: 1683676800000, doc_count: 32 },
],
},
},
}),
} as unknown as HttpSetup;
const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
() =>
useAlertsHistory({
http,
featureIds: [AlertConsumers.APM],
ruleId,
dateRange: { from: start, to: end },
}),
{
wrapper,
}
);
await act(async () => {
await waitFor(() => result.current.isSuccess);
});
expect(result.current.isLoading).toBeFalsy();
expect(result.current.isError).toBeFalsy();
expect(result.current.data.avgTimeToRecoverUS).toEqual(134959464.2857143);
expect(result.current.data.histogramTriggeredAlerts?.length).toEqual(31);
expect(result.current.data.totalTriggeredAlerts).toEqual(32);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* 2.0.
*/

import { useEffect, useRef } from 'react';
import type { HttpSetup } from '@kbn/core/public';
import {
ALERT_DURATION,
ALERT_RULE_UUID,
Expand All @@ -15,61 +13,81 @@ import {
ALERT_TIME_RANGE,
ValidFeatureId,
} from '@kbn/rule-data-utils';
import { type HttpSetup } from '@kbn/core/public';
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';
import { useQuery } from '@tanstack/react-query';
import { AggregationsDateHistogramBucketKeys } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';

interface Props {
export interface Props {
http: HttpSetup | undefined;
featureIds: ValidFeatureId[];
ruleId: string;
dateRange: {
from: string;
to: string;
};
}

interface FetchAlertsHistory {
totalTriggeredAlerts: number;
histogramTriggeredAlerts: estypes.AggregationsDateHistogramBucketKeys[];
error?: string;
histogramTriggeredAlerts: AggregationsDateHistogramBucketKeys[];
avgTimeToRecoverUS: number;
}

export function useAlertsHistory({ featureIds, ruleId, dateRange }: Props) {
const { http } = useKibana<InfraClientCoreStart>().services;
const abortCtrlRef = useRef(new AbortController());

const [state, refetch] = useAsyncFn(
() => {
abortCtrlRef.current.abort();
abortCtrlRef.current = new AbortController();
export interface UseAlertsHistory {
data: FetchAlertsHistory;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
}
export const EMPTY_ALERTS_HISTORY = {
totalTriggeredAlerts: 0,
histogramTriggeredAlerts: [] as AggregationsDateHistogramBucketKeys[],
avgTimeToRecoverUS: 0,
};
export function useAlertsHistory({ featureIds, ruleId, dateRange, http }: Props): UseAlertsHistory {
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: ['useAlertsHistory'],
queryFn: async ({ signal }) => {
if (!http) {
throw new Error('Http client is missing');
}
return fetchTriggeredAlertsHistory({
featureIds,
http,
ruleId,
dateRange,
signal: abortCtrlRef.current.signal,
signal,
});
},
[ruleId],
{ loading: true }
);

useEffect(() => {
refetch();
}, [refetch]);

const { value, error, loading } = state;
refetchOnWindowFocus: false,
});
return {
...value,
error,
loading,
refetch,
data: isInitialLoading ? EMPTY_ALERTS_HISTORY : data ?? EMPTY_ALERTS_HISTORY,
isLoading: isInitialLoading || isLoading || isRefetching,
isSuccess,
isError,
};
}

async function fetchTriggeredAlertsHistory({
interface AggsESResponse {
aggregations: {
avgTimeToRecoverUS: {
doc_count: number;
recoveryTime: {
value: number;
};
};
histogramTriggeredAlerts: {
buckets: AggregationsDateHistogramBucketKeys[];
};
};
hits: {
total: {
value: number;
};
};
}
export async function fetchTriggeredAlertsHistory({
featureIds,
http,
ruleId,
Expand All @@ -83,10 +101,10 @@ async function fetchTriggeredAlertsHistory({
from: string;
to: string;
};
signal: AbortSignal;
signal?: AbortSignal;
}): Promise<FetchAlertsHistory> {
return http
.post<estypes.SearchResponse<Record<string, unknown>>>(`${BASE_RAC_ALERTS_API_PATH}/find`, {
try {
const responseES = await http.post<AggsESResponse>(`${BASE_RAC_ALERTS_API_PATH}/find`, {
signal,
body: JSON.stringify({
size: 0,
Expand Down Expand Up @@ -134,25 +152,19 @@ async function fetchTriggeredAlertsHistory({
},
},
}),
})
.then(extractAlertsHistory);
});
const totalTriggeredAlerts = responseES.hits.total.value;
const avgTimeToRecoverUS = responseES.aggregations.avgTimeToRecoverUS.recoveryTime.value;
const histogramTriggeredAlerts = responseES.aggregations.histogramTriggeredAlerts.buckets;
return {
totalTriggeredAlerts,
histogramTriggeredAlerts,
avgTimeToRecoverUS,
};
} catch (error) {
throw new Error(
"Something went wrong while fetching alert history chart's data. Error:",
error
);
}
}

const extractAlertsHistory = (response: estypes.SearchResponse<Record<string, unknown>>) => {
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,
};
};
6 changes: 5 additions & 1 deletion x-pack/packages/observability/alert_details/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"target/**/*"
],
"kbn_references": [
"@kbn/i18n"
"@kbn/i18n",
"@kbn/core-http-browser",
"@kbn/rule-data-utils",
"@kbn/core",
"@kbn/rule-registry-plugin"
]
}
Loading

0 comments on commit 24260d7

Please sign in to comment.