mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Alert details page][Alert history chart] Apply group by on alert history chart (#180188)
Closes #176718 ## Summary In this PR, I will apply the group by information on the history chart of the following rules: 1. Log threshold  2. APM Latency threshold  3. SLO burn rate  I also update the custom threshold history chart to use instanceId for filtering alerts instead of queries. ## 🧪 How to test Check both the number of alerts on the annotation and also the data in the history chart to match the data for the defined group, in the following rules. 1. Log threshold 2. APM Latency threshold 3. SLO burn rate
This commit is contained in:
parent
30b313fefc
commit
31d08d2e80
12 changed files with 124 additions and 33 deletions
|
@ -59,6 +59,7 @@ describe('useAlertsHistory', () => {
|
|||
expect(result.current.isSuccess).toBeFalsy();
|
||||
expect(result.current.isLoading).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns no data when API error', async () => {
|
||||
const http = {
|
||||
post: jest.fn().mockImplementation(() => {
|
||||
|
@ -151,7 +152,7 @@ describe('useAlertsHistory', () => {
|
|||
expect(result.current.data.totalTriggeredAlerts).toEqual(32);
|
||||
});
|
||||
|
||||
it('calls http post including term queries', async () => {
|
||||
it('calls http post including instanceId query', async () => {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
const mockedHttpPost = jest.fn();
|
||||
|
@ -176,15 +177,56 @@ describe('useAlertsHistory', () => {
|
|||
featureIds: [AlertConsumers.APM],
|
||||
ruleId,
|
||||
dateRange: { from: start, to: end },
|
||||
queries: [
|
||||
{
|
||||
term: {
|
||||
'kibana.alert.group.value': {
|
||||
value: 'host=1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
instanceId: 'instance-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.instance.id":"instance-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,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls http post without * instanceId query', 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 },
|
||||
instanceId: '*',
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
|
@ -198,7 +240,6 @@ describe('useAlertsHistory', () => {
|
|||
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"}}},' +
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
*/
|
||||
|
||||
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_INSTANCE_ID,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_START,
|
||||
ALERT_STATUS,
|
||||
|
@ -27,7 +27,7 @@ export interface Props {
|
|||
from: string;
|
||||
to: string;
|
||||
};
|
||||
queries?: QueryDslQueryContainer[];
|
||||
instanceId?: string;
|
||||
}
|
||||
|
||||
interface FetchAlertsHistory {
|
||||
|
@ -47,12 +47,13 @@ export const EMPTY_ALERTS_HISTORY = {
|
|||
histogramTriggeredAlerts: [] as AggregationsDateHistogramBucketKeys[],
|
||||
avgTimeToRecoverUS: 0,
|
||||
};
|
||||
|
||||
export function useAlertsHistory({
|
||||
featureIds,
|
||||
ruleId,
|
||||
dateRange,
|
||||
http,
|
||||
queries,
|
||||
instanceId,
|
||||
}: Props): UseAlertsHistory {
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: ['useAlertsHistory'],
|
||||
|
@ -66,7 +67,7 @@ export function useAlertsHistory({
|
|||
ruleId,
|
||||
dateRange,
|
||||
signal,
|
||||
queries,
|
||||
instanceId,
|
||||
});
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
|
@ -103,7 +104,7 @@ export async function fetchTriggeredAlertsHistory({
|
|||
ruleId,
|
||||
dateRange,
|
||||
signal,
|
||||
queries = [],
|
||||
instanceId,
|
||||
}: {
|
||||
featureIds: ValidFeatureId[];
|
||||
http: HttpSetup;
|
||||
|
@ -113,7 +114,7 @@ export async function fetchTriggeredAlertsHistory({
|
|||
to: string;
|
||||
};
|
||||
signal?: AbortSignal;
|
||||
queries?: QueryDslQueryContainer[];
|
||||
instanceId?: string;
|
||||
}): Promise<FetchAlertsHistory> {
|
||||
try {
|
||||
const responseES = await http.post<AggsESResponse>(`${BASE_RAC_ALERTS_API_PATH}/find`, {
|
||||
|
@ -129,7 +130,15 @@ export async function fetchTriggeredAlertsHistory({
|
|||
[ALERT_RULE_UUID]: ruleId,
|
||||
},
|
||||
},
|
||||
...queries,
|
||||
...(instanceId && instanceId !== '*'
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
[ALERT_INSTANCE_ID]: instanceId,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
range: {
|
||||
[ALERT_TIME_RANGE]: dateRange,
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
ALERT_END,
|
||||
ALERT_EVALUATION_THRESHOLD,
|
||||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_START,
|
||||
|
@ -220,6 +221,7 @@ export function AlertDetailsAppSection({
|
|||
<EuiFlexItem grow={false}>
|
||||
<LatencyAlertsHistoryChart
|
||||
ruleId={alert.fields[ALERT_RULE_UUID]}
|
||||
alertInstanceId={alert.fields[ALERT_INSTANCE_ID]}
|
||||
serviceName={serviceName}
|
||||
start={historicalRange.start}
|
||||
end={historicalRange.end}
|
||||
|
|
|
@ -51,6 +51,7 @@ interface LatencyAlertsHistoryChartProps {
|
|||
environment: string;
|
||||
timeZone: string;
|
||||
ruleId: string;
|
||||
alertInstanceId?: string;
|
||||
}
|
||||
export function LatencyAlertsHistoryChart({
|
||||
serviceName,
|
||||
|
@ -61,6 +62,7 @@ export function LatencyAlertsHistoryChart({
|
|||
environment,
|
||||
timeZone,
|
||||
ruleId,
|
||||
alertInstanceId,
|
||||
}: LatencyAlertsHistoryChartProps) {
|
||||
const preferred = usePreferredDataSourceAndBucketSize({
|
||||
start,
|
||||
|
@ -144,6 +146,7 @@ export function LatencyAlertsHistoryChart({
|
|||
featureIds: [AlertConsumers.APM],
|
||||
ruleId,
|
||||
dateRange: { from: start, to: end },
|
||||
instanceId: alertInstanceId,
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
|
|
|
@ -28,8 +28,17 @@ import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana';
|
|||
import { type PartialCriterion } from '../../../../../../common/alerting/logs/log_threshold';
|
||||
import { CriterionPreview } from '../../expression_editor/criterion_preview_chart';
|
||||
import { PartialRuleParams } from '../../../../../../common/alerting/logs/log_threshold';
|
||||
import type { Group } from '../types';
|
||||
|
||||
const LogsHistoryChart = ({ rule }: { rule: Rule<PartialRuleParams> }) => {
|
||||
const LogsHistoryChart = ({
|
||||
rule,
|
||||
instanceId,
|
||||
groups,
|
||||
}: {
|
||||
rule: Rule<PartialRuleParams>;
|
||||
instanceId?: string;
|
||||
groups?: Group[];
|
||||
}) => {
|
||||
const { http, notifications } = useKibanaContextForPlugin().services;
|
||||
// Show the Logs History Chart ONLY if we have one criteria
|
||||
// So always pull the first criteria
|
||||
|
@ -42,6 +51,7 @@ const LogsHistoryChart = ({ rule }: { rule: Rule<PartialRuleParams> }) => {
|
|||
const executionTimeRange = {
|
||||
gte: DateMath.parse(dateRange.from)!.valueOf(),
|
||||
lte: DateMath.parse(dateRange.to, { roundUp: true })!.valueOf(),
|
||||
buckets: 30,
|
||||
};
|
||||
|
||||
const {
|
||||
|
@ -53,6 +63,7 @@ const LogsHistoryChart = ({ rule }: { rule: Rule<PartialRuleParams> }) => {
|
|||
featureIds: [AlertConsumers.LOGS],
|
||||
ruleId: rule.id,
|
||||
dateRange,
|
||||
instanceId,
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
|
@ -180,11 +191,12 @@ const LogsHistoryChart = ({ rule }: { rule: Rule<PartialRuleParams> }) => {
|
|||
markerPosition={Position.Top}
|
||||
/>,
|
||||
]}
|
||||
ruleParams={rule.params}
|
||||
ruleParams={{ ...rule.params, timeSize: 1, timeUnit: 'd' }}
|
||||
logViewReference={rule.params.logView}
|
||||
chartCriterion={criteria as PartialCriterion}
|
||||
showThreshold={true}
|
||||
executionTimeRange={executionTimeRange}
|
||||
filterSeriesByGroupName={groups?.map((group) => group.value).join(', ')}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
ALERT_CONTEXT,
|
||||
ALERT_END,
|
||||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_START,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import moment from 'moment';
|
||||
|
@ -34,6 +35,7 @@ import { Threshold } from '../../../common/components/threshold';
|
|||
import { LogRateAnalysis } from './components/log_rate_analysis';
|
||||
import { LogThresholdCountChart, LogThresholdRatioChart } from './components/threhsold_chart';
|
||||
import { useLicense } from '../../../../hooks/use_license';
|
||||
import type { Group } from './types';
|
||||
|
||||
const LogsHistoryChart = React.lazy(() => import('./components/logs_history_chart'));
|
||||
const formatThreshold = (threshold: number) => String(threshold);
|
||||
|
@ -63,6 +65,16 @@ const AlertDetailsAppSection = ({
|
|||
.filter(identity)
|
||||
.join(' AND ')
|
||||
: '';
|
||||
const groups: Group[] | undefined = rule.params.groupBy
|
||||
? rule.params.groupBy.flatMap((field) => {
|
||||
const value: string = get(
|
||||
alert.fields[ALERT_CONTEXT],
|
||||
['groupByKeys', ...field.split('.')],
|
||||
null
|
||||
);
|
||||
return value ? { field, value } : [];
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const { derivedDataView } = useLogView({
|
||||
initialLogViewReference: rule.params.logView,
|
||||
|
@ -230,7 +242,12 @@ const AlertDetailsAppSection = ({
|
|||
rule.params.criteria.length === 1 && (
|
||||
<EuiFlexItem>
|
||||
<LogsHistoryChart
|
||||
rule={{ ...rule, params: { ...rule.params, timeSize: 12, timeUnit: 'h' } }}
|
||||
rule={{
|
||||
...rule,
|
||||
params: { ...rule.params, timeSize: 12, timeUnit: 'h' },
|
||||
}}
|
||||
instanceId={alert.fields[ALERT_INSTANCE_ID]}
|
||||
groups={groups}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
|
|
|
@ -14,3 +14,8 @@ export interface AlertDetailsAppSectionProps {
|
|||
alert: TopAlert<Record<string, any>>;
|
||||
setAlertSummaryFields: React.Dispatch<React.SetStateAction<AlertSummaryField[] | undefined>>;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
field: string;
|
||||
value: string;
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ interface Props {
|
|||
showThreshold: boolean;
|
||||
executionTimeRange?: ExecutionTimeRange;
|
||||
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
|
||||
filterSeriesByGroupName?: string[];
|
||||
filterSeriesByGroupName?: string;
|
||||
}
|
||||
|
||||
export const CriterionPreview: React.FC<Props> = ({
|
||||
|
@ -107,7 +107,9 @@ export const CriterionPreview: React.FC<Props> = ({
|
|||
return (
|
||||
<CriterionPreviewChart
|
||||
buckets={
|
||||
!chartAlertParams.groupBy || chartAlertParams.groupBy.length === 0
|
||||
executionTimeRange?.buckets
|
||||
? executionTimeRange.buckets
|
||||
: !chartAlertParams.groupBy || chartAlertParams.groupBy.length === 0
|
||||
? NUM_BUCKETS
|
||||
: NUM_BUCKETS / 4
|
||||
} // Display less data for groups due to space limitations
|
||||
|
@ -130,7 +132,7 @@ interface ChartProps {
|
|||
showThreshold: boolean;
|
||||
executionTimeRange?: ExecutionTimeRange;
|
||||
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
|
||||
filterSeriesByGroupName?: string[];
|
||||
filterSeriesByGroupName?: string;
|
||||
}
|
||||
|
||||
const CriterionPreviewChart: React.FC<ChartProps> = ({
|
||||
|
@ -190,7 +192,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
|
|||
return series;
|
||||
}
|
||||
if (filterSeriesByGroupName && filterSeriesByGroupName.length) {
|
||||
return series.filter((item) => filterSeriesByGroupName.includes(item.id));
|
||||
return series.filter((item) => filterSeriesByGroupName === item.id);
|
||||
}
|
||||
const sortedByMax = series.sort((a, b) => {
|
||||
const aMax = Math.max(...a.points.map((point) => point.value));
|
||||
|
|
|
@ -135,6 +135,7 @@ export interface InfraHttpError extends IHttpFetchError {
|
|||
export interface ExecutionTimeRange {
|
||||
gte: number;
|
||||
lte: number;
|
||||
buckets?: number;
|
||||
}
|
||||
|
||||
type PropsOf<T> = T extends React.ComponentType<infer ComponentProps> ? ComponentProps : never;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Group } from '../types';
|
|||
|
||||
/*
|
||||
* groupFieldName
|
||||
* In some cases, like AAD indices, the field name for group value is differen from group.field,
|
||||
* In some cases, like AAD indices, the field name for group value is different from group.field,
|
||||
* in AAD case, it is ALERT_GROUP_VALUE (`kibana.alert.group.value`). groupFieldName allows
|
||||
* passing a different field name to be used in the query.
|
||||
*/
|
||||
|
|
|
@ -22,13 +22,10 @@ import {
|
|||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALERT_GROUP, ALERT_GROUP_VALUE, type AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { ALERT_GROUP, ALERT_INSTANCE_ID, type AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { useAlertsHistory } from '@kbn/observability-alert-details';
|
||||
import { convertTo } from '../../../../../common/utils/formatters';
|
||||
import {
|
||||
getGroupFilters,
|
||||
getGroupQueries,
|
||||
} from '../../../../../common/custom_threshold_rule/helpers/get_group';
|
||||
import { getGroupFilters } from '../../../../../common/custom_threshold_rule/helpers/get_group';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { AlertParams } from '../../types';
|
||||
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
|
||||
|
@ -54,6 +51,7 @@ export function AlertHistoryChart({ rule, dataView, alert }: Props) {
|
|||
const ruleParams = rule.params as RuleTypeParams & AlertParams;
|
||||
const criterion = rule.params.criteria[0];
|
||||
const groups = alert.fields[ALERT_GROUP];
|
||||
const instanceId = alert.fields[ALERT_INSTANCE_ID];
|
||||
const featureIds = [rule.consumer as AlertConsumers];
|
||||
|
||||
const {
|
||||
|
@ -65,7 +63,7 @@ export function AlertHistoryChart({ rule, dataView, alert }: Props) {
|
|||
featureIds,
|
||||
ruleId: rule.id,
|
||||
dateRange,
|
||||
queries: getGroupQueries(groups, ALERT_GROUP_VALUE),
|
||||
instanceId,
|
||||
});
|
||||
|
||||
// Only show alert history chart if there is only one condition
|
||||
|
|
|
@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useAlertsHistory } from '@kbn/observability-alert-details';
|
||||
import rison from '@kbn/rison';
|
||||
import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
|
||||
import { ALERT_INSTANCE_ID, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
|
||||
import { GetSLOResponse } from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
@ -52,6 +52,7 @@ export function AlertsHistoryPanel({ rule, slo, alert, isLoading }: Props) {
|
|||
to: 'now',
|
||||
},
|
||||
http,
|
||||
instanceId: alert.fields[ALERT_INSTANCE_ID],
|
||||
});
|
||||
|
||||
const actionGroup = getActionGroupFromReason(alert.reason);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue