[AO] - Add chart series filter capability for the Logs Alert Details page (#154335)

## Summary

It closes #154262 

### Before:

![](https://user-images.githubusercontent.com/6838659/229560256-e8965afd-fd45-4451-9b45-4496a477283b.png)


### After, we can filter the shown series on the chart alert-based


![](https://user-images.githubusercontent.com/6838659/229560443-279d26a7-a3dd-4ebb-8371-c6e35d50c555.png)
This commit is contained in:
Faisal Kanout 2023-04-04 16:16:30 +02:00 committed by GitHub
parent 62868e7fc0
commit e57c853329
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 44 additions and 4 deletions

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
// It's simple function to be shared, but it is required on both sides server and frontend
// We need to get consistent group names when any changes occurs.
export const getChartGroupNames = (fields: string[]) => fields.join(', ');

View file

@ -6,8 +6,10 @@
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { ALERT_DURATION, ALERT_END } from '@kbn/rule-data-utils';
import compact from 'lodash/compact';
import moment from 'moment';
import React from 'react';
import { getChartGroupNames } from '../../../../../common/utils/get_chart_group_names';
import { type PartialCriterion } from '../../../../../common/alerting/logs/log_threshold';
import { CriterionPreview } from '../expression_editor/criterion_preview_chart';
import { AlertAnnotation } from './components/alert_annotation';
@ -21,6 +23,25 @@ const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) =>
.asMilliseconds();
const alertDurationMS = alert.fields[ALERT_DURATION]! / 1000;
const TWENTY_TIMES_RULE_WINDOW_MS = 20 * ruleWindowSizeMS;
/**
* The `CriterionPreview` chart shows all the series/data stacked when there is a GroupBy in the rule parameters.
* e.g., `host.name`, the chart will show stacks of data by hostname.
* We only need the chart to show the series that is related to the selected alert.
* The chart series are built based on the GroupBy in the rule params
* Each series have an id which is the just a joining of fields value of the GroupBy `getChartGroupNames`
* We filter down the series using this group name
*/
const alertFieldsFromGroupBy = compact(
rule.params.groupBy?.map((fieldNameGroupBy) => {
const field = Object.keys(alert.fields).find(
(alertFiledName) => alertFiledName === fieldNameGroupBy
);
if (field) return alert.fields[field];
})
);
const selectedSeries = getChartGroupNames(alertFieldsFromGroupBy);
/**
* This is part or the requirements (RFC).
* If the alert is less than 20 units of `FOR THE LAST <x> <units>` then we should draw a time range of 20 units.
@ -52,6 +73,7 @@ const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) =>
showThreshold={true}
executionTimeRange={{ gte: rangeFrom, lte: rangeTo }}
annotations={[<AlertAnnotation alertStarted={alert.start} />]}
filterSeriesByGroupName={[selectedSeries]}
/>
</EuiFlexItem>
);

View file

@ -11,5 +11,5 @@ import { PartialRuleParams } from '../../../../../common/alerting/logs/log_thres
export interface AlertDetailsAppSectionProps {
rule: Rule<PartialRuleParams>;
alert: TopAlert;
alert: TopAlert<Record<string, any>>;
}

View file

@ -60,6 +60,7 @@ interface Props {
showThreshold: boolean;
executionTimeRange?: ExecutionTimeRange;
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
filterSeriesByGroupName?: string[];
}
export const CriterionPreview: React.FC<Props> = ({
@ -69,6 +70,7 @@ export const CriterionPreview: React.FC<Props> = ({
showThreshold,
executionTimeRange,
annotations,
filterSeriesByGroupName,
}) => {
const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => {
const { field, comparator, value } = chartCriterion;
@ -114,6 +116,7 @@ export const CriterionPreview: React.FC<Props> = ({
showThreshold={showThreshold}
executionTimeRange={executionTimeRange}
annotations={annotations}
filterSeriesByGroupName={filterSeriesByGroupName}
/>
);
};
@ -126,6 +129,7 @@ interface ChartProps {
showThreshold: boolean;
executionTimeRange?: ExecutionTimeRange;
annotations?: Array<ReactElement<typeof RectAnnotation | typeof LineAnnotation>>;
filterSeriesByGroupName?: string[];
}
const CriterionPreviewChart: React.FC<ChartProps> = ({
@ -136,6 +140,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
showThreshold,
executionTimeRange,
annotations,
filterSeriesByGroupName,
}) => {
const { uiSettings } = useKibana().services;
const isDarkMode = uiSettings?.get('theme:darkMode') || false;
@ -184,7 +189,9 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
if (!isGrouped) {
return series;
}
if (filterSeriesByGroupName && filterSeriesByGroupName.length) {
return series.filter((item) => filterSeriesByGroupName.includes(item.id));
}
const sortedByMax = series.sort((a, b) => {
const aMax = Math.max(...a.points.map((point) => point.value));
const bMax = Math.max(...b.points.map((point) => point.value));
@ -192,7 +199,7 @@ const CriterionPreviewChart: React.FC<ChartProps> = ({
});
const sortedSeries = (!isAbove && !isBelow) || isAbove ? sortedByMax : sortedByMax.reverse();
return sortedSeries.slice(0, GROUP_LIMIT);
}, [series, isGrouped, isAbove, isBelow]);
}, [isGrouped, filterSeriesByGroupName, series, isAbove, isBelow]);
const barSeries = useMemo(() => {
return filteredSeries.reduce<Array<{ timestamp: number; value: number; groupBy: string }>>(

View file

@ -27,6 +27,7 @@ import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields';
import { ecsFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/ecs_field_map';
import { getChartGroupNames } from '../../../../common/utils/get_chart_group_names';
import {
RuleParams,
ruleParamsRT,
@ -484,7 +485,7 @@ const getReducedGroupByResults = (
): ReducedGroupByResults => {
const getGroupName = (
key: GroupedSearchQueryResponse['aggregations']['groups']['buckets'][0]['key']
) => Object.values(key).join(', ');
) => getChartGroupNames(Object.values(key));
const reducedGroupByResults: ReducedGroupByResults = [];
if (isOptimizedGroupedSearchQueryResponse(results)) {