Skip to content

Commit

Permalink
[Security Solution][Serverless] - Improve security solution performan…
Browse files Browse the repository at this point in the history
…ce (elastic#194241)

## Summary

The goal of this PR is to improve the default performance of many of our
security solution views.

1. Upon scale testing, it was observed that the default events histogram
aggregation was a source of application slowness, so to improve the
performance of the default security experience, we've made the default
breakdown for the events histogram `No Breakdown` similar to what is
seen in the default discover histogram experience.

2. After looking through some telemetry, it was observed that the field
list query run in the background for timeline can also take a
significant amount of time based on the user's field count, so we now
only run that query after timeline has been opened.

### Demos
#### 1. By default the events visualizations on the overview and explore
events pages will not have an aggregation. The user will have to
manually select the breakdown they desire:
elastic@d354d27


https://github.com/user-attachments/assets/a6d6987b-73fc-4735-9c37-973917c2fa2d


#### 2. Timeline fields list will only load after the first interaction
with timeline:
elastic@ad55726

**Before:**


https://github.com/user-attachments/assets/0ad2e903-ac15-4daa-925b-da8ad05e80dd


**After:**


https://github.com/user-attachments/assets/27d5d3d5-02c8-49b5-b699-239ebc36b16c
  • Loading branch information
michaelolo24 authored and angorayc committed Oct 1, 2024
1 parent f20738a commit b8983be
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EventsQueryTabBody, ALERTS_EVENTS_HISTOGRAM_ID } from './events_query_t
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import { licenseService } from '../../hooks/use_license';
import { mockHistory } from '../../mock/router';
import { DEFAULT_EVENTS_STACK_BY_VALUE } from './histogram_configurations';

const mockGetDefaultControlColumn = jest.fn();
jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({
Expand Down Expand Up @@ -144,7 +145,7 @@ describe('EventsQueryTabBody', () => {
);

expect(result.getByTestId('header-section-supplements').querySelector('select')?.value).toEqual(
'event.action'
DEFAULT_EVENTS_STACK_BY_VALUE
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { getEventsHistogramLensAttributes } from '../visualization_actions/lens_
import type { MatrixHistogramConfigs, MatrixHistogramOption } from '../matrix_histogram/types';
import * as i18n from './translations';

const DEFAULT_EVENTS_STACK_BY = 'event.action';
export const NO_BREAKDOWN_STACK_BY_VALUE = 'no_breakdown';

export const DEFAULT_EVENTS_STACK_BY_VALUE = NO_BREAKDOWN_STACK_BY_VALUE;

export const getSubtitleFunction =
(defaultNumberFormat: string, isAlert: boolean) => (totalCount: number) =>
Expand All @@ -20,6 +22,10 @@ export const getSubtitleFunction =
}`;

export const eventsStackByOptions: MatrixHistogramOption[] = [
{
text: i18n.EVENTS_GRAPH_NO_BREAKDOWN_TITLE,
value: NO_BREAKDOWN_STACK_BY_VALUE,
},
{
text: 'event.action',
value: 'event.action',
Expand All @@ -36,7 +42,8 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [

export const eventsHistogramConfig: MatrixHistogramConfigs = {
defaultStackByOption:
eventsStackByOptions.find((o) => o.text === DEFAULT_EVENTS_STACK_BY) ?? eventsStackByOptions[0],
eventsStackByOptions.find((o) => o.value === DEFAULT_EVENTS_STACK_BY_VALUE) ??
eventsStackByOptions[0],
stackByOptions: eventsStackByOptions,
subtitle: undefined,
title: i18n.EVENTS_GRAPH_TITLE,
Expand All @@ -58,7 +65,7 @@ const DEFAULT_STACK_BY = 'event.module';

export const alertsHistogramConfig: MatrixHistogramConfigs = {
defaultStackByOption:
alertsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0],
alertsStackByOptions.find((o) => o.value === DEFAULT_STACK_BY) ?? alertsStackByOptions[0],
stackByOptions: alertsStackByOptions,
title: i18n.ALERTS_GRAPH_TITLE,
getLensAttributes: getExternalAlertLensAttributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ export const SHOW_EXTERNAL_ALERTS = i18n.translate(
export const EVENTS_GRAPH_TITLE = i18n.translate('xpack.securitySolution.eventsGraphTitle', {
defaultMessage: 'Events',
});

export const EVENTS_GRAPH_NO_BREAKDOWN_TITLE = i18n.translate(
'xpack.securitySolution.eventsHistogram.selectOptions.noBreakDownLabel',
{
defaultMessage: 'No breakdown',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/uti
import { VisualizationEmbeddable } from '../visualization_actions/visualization_embeddable';
import { useVisualizationResponse } from '../visualization_actions/use_visualization_response';
import type { SourcererScopeName } from '../../../sourcerer/store/model';
import { NO_BREAKDOWN_STACK_BY_VALUE } from '../events_tab/histogram_configurations';

export type MatrixHistogramComponentProps = MatrixHistogramQueryProps &
MatrixHistogramConfigs & {
Expand Down Expand Up @@ -165,6 +166,13 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
[isPtrIncluded, filterQuery]
);

// If the user selected the `No breakdown` option, we shouldn't perform the aggregation
const stackByField = useMemo(() => {
return selectedStackByOption.value === NO_BREAKDOWN_STACK_BY_VALUE
? undefined
: selectedStackByOption.value;
}, [selectedStackByOption.value]);

if (hideHistogram) {
return null;
}
Expand Down Expand Up @@ -216,7 +224,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
id={visualizationId}
inspectTitle={title as string}
lensAttributes={lensAttributes}
stackByField={selectedStackByOption.value}
stackByField={stackByField}
timerange={timerange}
/>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { GetLensAttributes, LensAttributes } from '../visualization_actions

export interface MatrixHistogramOption {
text: string;
value: string;
value: string | undefined;
}

export type GetSubTitle = (count: number) => string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { wrapper } from '../../mocks';

import { useLensAttributes } from '../../use_lens_attributes';

import { getEventsHistogramLensAttributes } from './events';
import { getEventsHistogramLensAttributes, stackByFieldAccessorId } from './events';

jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('0039eb0c-9a1a-4687-ae54-0f4e239bec75'),
Expand Down Expand Up @@ -497,4 +497,41 @@ describe('getEventsHistogramLensAttributes', () => {
})
);
});

it('should render the layer for the stackByField when provided', () => {
const { result } = renderHook(
() =>
useLensAttributes({
getLensAttributes: getEventsHistogramLensAttributes,
stackByField: 'event.dataset',
}),
{ wrapper }
);

expect(result?.current?.state?.visualization).toEqual(
expect.objectContaining({
layers: expect.arrayContaining([
expect.objectContaining({ splitAccessor: stackByFieldAccessorId }),
]),
})
);
});

it('should not render the layer for the stackByField is undefined', () => {
const { result } = renderHook(
() =>
useLensAttributes({
getLensAttributes: getEventsHistogramLensAttributes,
}),
{ wrapper }
);

expect(result?.current?.state?.visualization).toEqual(
expect.objectContaining({
layers: expect.arrayContaining([
expect.not.objectContaining({ splitAccessor: stackByFieldAccessorId }),
]),
})
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { v4 as uuidv4 } from 'uuid';
import type { GetLensAttributes } from '../../types';

const layerId = uuidv4();
// Exported for testing purposes
export const stackByFieldAccessorId = '34919782-4546-43a5-b668-06ac934d3acd';

export const getEventsHistogramLensAttributes: GetLensAttributes = (
stackByField = 'event.action',
stackByField,
extraOptions = {}
) => {
return {
Expand All @@ -37,7 +39,7 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = (
showGridlines: false,
layerType: 'data',
xAccessor: 'aac9d7d0-13a3-480a-892b-08207a787926',
splitAccessor: '34919782-4546-43a5-b668-06ac934d3acd',
splitAccessor: stackByField ? stackByFieldAccessorId : undefined,
},
],
yRightExtent: {
Expand Down Expand Up @@ -83,30 +85,32 @@ export const getEventsHistogramLensAttributes: GetLensAttributes = (
sourceField: '___records___',
params: { emptyAsNull: true },
},
'34919782-4546-43a5-b668-06ac934d3acd': {
label: `Top values of ${stackByField}`,
dataType: 'string',
operationType: 'terms',
scale: 'ordinal',
sourceField: `${stackByField}`,
isBucketed: true,
params: {
size: 10,
orderBy: {
type: 'column',
columnId: 'e09e0380-0740-4105-becc-0a4ca12e3944',
},
orderDirection: 'desc',
otherBucket: true,
missingBucket: false,
parentFormat: {
id: 'terms',
...(stackByField && {
[stackByFieldAccessorId]: {
label: `Top values of ${stackByField}`,
dataType: 'string',
operationType: 'terms',
scale: 'ordinal',
sourceField: `${stackByField}`,
isBucketed: true,
params: {
size: 10,
orderBy: {
type: 'column',
columnId: 'e09e0380-0740-4105-becc-0a4ca12e3944',
},
orderDirection: 'desc',
otherBucket: true,
missingBucket: false,
parentFormat: {
id: 'terms',
},
},
},
},
}),
},
columnOrder: [
'34919782-4546-43a5-b668-06ac934d3acd',
...(stackByField ? [stackByFieldAccessorId] : []),
'aac9d7d0-13a3-480a-892b-08207a787926',
'e09e0380-0740-4105-becc-0a4ca12e3944',
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useSourcererDataView } from '../../../sourcerer/containers';
import { kpiHostMetricLensAttributes } from './lens_attributes/hosts/kpi_host_metric';
import { useRouteSpy } from '../../utils/route/use_route_spy';
import { SecurityPageName } from '../../../app/types';
import { getEventsHistogramLensAttributes } from './lens_attributes/common/events';

jest.mock('../../../sourcerer/containers');
jest.mock('../../utils/route/use_route_spy', () => ({
Expand Down Expand Up @@ -212,6 +213,25 @@ describe('useLensAttributes', () => {
]);
});

it('should not set splitAccessor if stackByField is undefined', () => {
const { result } = renderHook(
() =>
useLensAttributes({
getLensAttributes: getEventsHistogramLensAttributes,
stackByField: undefined,
}),
{ wrapper }
);

expect(result?.current?.state?.visualization).toEqual(
expect.objectContaining({
layers: expect.arrayContaining([
expect.objectContaining({ seriesType: 'bar_stacked', splitAccessor: undefined }),
]),
})
);
});

it('should return null if no indices exist', () => {
(useSourcererDataView as jest.Mock).mockReturnValue({
dataViewId: 'security-solution-default',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const useLensAttributes = ({
() =>
lensAttributes ??
((getLensAttributes &&
stackByField &&
stackByField !== null &&
getLensAttributes(stackByField, extraOptions)) as LensAttributes),
[extraOptions, getLensAttributes, lensAttributes, stackByField]
);
Expand All @@ -82,7 +82,7 @@ export const useLensAttributes = ({
const lensAttrsWithInjectedData = useMemo(() => {
if (
lensAttributes == null &&
(getLensAttributes == null || stackByField == null || stackByField?.length === 0)
(getLensAttributes == null || stackByField === null || stackByField?.length === 0)
) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useKibana, useUiSetting$ } from '../../../common/lib/kibana';
import {
eventsStackByOptions,
eventsHistogramConfig,
NO_BREAKDOWN_STACK_BY_VALUE,
} from '../../../common/components/events_tab/histogram_configurations';
import { HostsTableType } from '../../../explore/hosts/store/model';
import type { GlobalTimeArgs } from '../../../common/containers/use_global_time';
Expand All @@ -36,7 +37,7 @@ import { useFormatUrl } from '../../../common/components/link_to';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
import type { SourcererScopeName } from '../../../sourcerer/store/model';

const DEFAULT_STACK_BY = 'event.dataset';
const DEFAULT_STACK_BY = NO_BREAKDOWN_STACK_BY_VALUE;

const ID = 'eventsByDatasetOverview';
const CHART_HEIGHT = 160;
Expand Down Expand Up @@ -156,7 +157,7 @@ const EventsByDatasetComponent: React.FC<Props> = ({
defaultStackByOption:
onlyField != null
? getHistogramOption(onlyField)
: eventsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ??
: eventsStackByOptions.find((o) => o.value === DEFAULT_STACK_BY) ??
eventsStackByOptions[0],
legendPosition: Position.Right,
subtitle: (totalCount: number) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ const TestComponent = (props: Partial<ComponentProps<typeof QueryTabContent>>) =

const dispatch = useDispatch();

// populating timeline so that it is not blank
useEffect(() => {
// Unified field list can be a culprit for long load times, so we wait for the timeline to be interacted with to load
dispatch(timelineActions.showTimeline({ id: TimelineId.test, show: true }));

// populating timeline so that it is not blank
dispatch(
timelineActions.applyKqlFilterQuery({
id: TimelineId.test,
Expand Down
Loading

0 comments on commit b8983be

Please sign in to comment.