[RCA] Events timeline !! (#193265)

## Summary

Events timeline !!

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/c00c2368-5f7e-4e5e-a6a1-cbcfacb859cd">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2024-10-07 15:41:24 +02:00 committed by GitHub
parent 2881b0423d
commit 89f2802505
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 351 additions and 13 deletions

View file

@ -13,6 +13,7 @@ import { eventSchema } from '../schema';
const eventResponseSchema = eventSchema;
type EventResponse = z.output<typeof eventResponseSchema>;
type EventSchema = z.output<typeof eventSchema>;
export { eventResponseSchema };
export type { EventResponse };
export type { EventResponse, EventSchema };

View file

@ -0,0 +1,16 @@
/*
* 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 { BrushEvent } from '@elastic/charts';
import moment from 'moment';
export function getBrushData(e: BrushEvent) {
const [from, to] = [Number(e.x?.[0]), Number(e.x?.[1])];
const [fromUtc, toUtc] = [moment(from).format(), moment(to).format()];
return { from: fromUtc, to: toUtc };
}

View file

@ -53,6 +53,7 @@ export function getMockInvestigateAppContext(): DeeplyMockedKeys<InvestigateAppK
});
}),
},
charts: {} as any,
};
const core = coreMock.createStart();

View file

@ -11,6 +11,7 @@
"investigate",
"observabilityShared",
"lens",
"charts",
"dataViews",
"data",
"embeddable",
@ -25,7 +26,7 @@
"requiredBundles": [
"esql",
"kibanaReact",
"kibanaUtils",
"kibanaUtils"
],
"optionalPlugins": ["observabilityAIAssistant"],
"extraPublicDirs": []

View file

@ -12,6 +12,8 @@ export const investigationKeys = {
userProfiles: (profileIds: Set<string>) =>
[...investigationKeys.all, 'userProfiles', ...profileIds] as const,
tags: () => [...investigationKeys.all, 'tags'] as const,
events: (rangeFrom?: string, rangeTo?: string) =>
[...investigationKeys.all, 'events', rangeFrom, rangeTo] as const,
stats: () => [...investigationKeys.all, 'stats'] as const,
lists: () => [...investigationKeys.all, 'list'] as const,
list: (params: { page: number; perPage: number; search?: string; filter?: string }) =>

View file

@ -0,0 +1,70 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { useQuery } from '@tanstack/react-query';
import { GetEventsResponse } from '@kbn/investigation-shared';
import { investigationKeys } from './query_key_factory';
import { useKibana } from './use_kibana';
export interface Response {
isInitialLoading: boolean;
isLoading: boolean;
isRefetching: boolean;
isSuccess: boolean;
isError: boolean;
data?: GetEventsResponse;
}
export function useFetchEvents({
rangeFrom,
rangeTo,
}: {
rangeFrom?: string;
rangeTo?: string;
}): Response {
const {
core: {
http,
notifications: { toasts },
},
} = useKibana();
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
queryKey: investigationKeys.events(rangeFrom, rangeTo),
queryFn: async ({ signal }) => {
return await http.get<GetEventsResponse>(`/api/observability/events`, {
query: {
rangeFrom,
rangeTo,
},
version: '2023-10-31',
signal,
});
},
cacheTime: 600 * 1000, // 10_minutes
staleTime: 0,
refetchOnWindowFocus: false,
retry: false,
onError: (error: Error) => {
toasts.addError(error, {
title: i18n.translate('xpack.investigateApp.events.fetch.error', {
defaultMessage: 'Something went wrong while fetching the events',
}),
});
},
});
return {
data,
isInitialLoading,
isLoading,
isRefetching,
isSuccess,
isError,
};
}

View file

@ -0,0 +1,34 @@
/*
* 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 { LineAnnotation, AnnotationDomainType } from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import React from 'react';
import moment from 'moment';
import { EventSchema } from '@kbn/investigation-shared';
export const AlertEvent = ({ event }: { event: EventSchema }) => {
return (
<LineAnnotation
id={event.id}
domainType={AnnotationDomainType.XDomain}
marker={
<span>
<EuiIcon style={{ marginTop: -16 }} type="dot" size="l" color="danger" />
</span>
}
markerPosition="bottom"
dataValues={[
{
dataValue: moment(event.timestamp).valueOf(),
header: moment(event.timestamp).format('lll'),
details: event.description,
},
]}
/>
);
};

View file

@ -0,0 +1,36 @@
/*
* 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 moment from 'moment';
import { AnnotationDomainType, LineAnnotation } from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import { EventSchema } from '@kbn/investigation-shared';
export function AnnotationEvent({ event }: { event: EventSchema }) {
const timestamp = event.timestamp;
return (
<LineAnnotation
id={event.id}
domainType={AnnotationDomainType.XDomain}
dataValues={[
{
dataValue: moment(timestamp).valueOf(),
details: event.description,
header: moment(event.timestamp).format('lll'),
},
]}
marker={
<span>
<EuiIcon style={{ marginTop: -16 }} type="dot" size="l" />
</span>
}
markerPosition="bottom"
/>
);
}

View file

@ -0,0 +1,114 @@
/*
* 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, { useMemo, useRef } from 'react';
import moment from 'moment';
import { Chart, Axis, AreaSeries, Position, ScaleType, Settings } from '@elastic/charts';
import { useActiveCursor } from '@kbn/charts-plugin/public';
import { EuiSkeletonText } from '@elastic/eui';
import { getBrushData } from '@kbn/observability-utils/chart/utils';
import { AnnotationEvent } from './annotation_event';
import { TIME_LINE_THEME } from './timeline_theme';
import { useFetchEvents } from '../../../../hooks/use_fetch_events';
import { useInvestigation } from '../../contexts/investigation_context';
import { useKibana } from '../../../../hooks/use_kibana';
import { AlertEvent } from './alert_event';
export const EventsTimeLine = () => {
const { dependencies } = useKibana();
const baseTheme = dependencies.start.charts.theme.useChartsBaseTheme();
const { globalParams, updateInvestigationParams } = useInvestigation();
const { data: events, isLoading } = useFetchEvents({
rangeFrom: globalParams.timeRange.from,
rangeTo: globalParams.timeRange.to,
});
const chartRef = useRef(null);
const handleCursorUpdate = useActiveCursor(dependencies.start.charts.activeCursor, chartRef, {
isDateHistogram: true,
});
const data = useMemo(() => {
const points = [
{ x: moment(globalParams.timeRange.from).valueOf(), y: 0 },
{ x: moment(globalParams.timeRange.to).valueOf(), y: 0 },
];
// adding 100 fake points to the chart so the chart shows cursor on hover
for (let i = 0; i < 100; i++) {
const diff =
moment(globalParams.timeRange.to).valueOf() - moment(globalParams.timeRange.from).valueOf();
points.push({ x: moment(globalParams.timeRange.from).valueOf() + (diff / 100) * i, y: 0 });
}
return points;
}, [globalParams.timeRange.from, globalParams.timeRange.to]);
if (isLoading) {
return <EuiSkeletonText />;
}
const alertEvents = events?.filter((evt) => evt.eventType === 'alert');
const annotations = events?.filter((evt) => evt.eventType === 'annotation');
return (
<>
<Chart size={['100%', 100]} ref={chartRef}>
<Settings
xDomain={{
min: moment(globalParams.timeRange.from).valueOf(),
max: moment(globalParams.timeRange.to).valueOf(),
}}
theme={TIME_LINE_THEME}
baseTheme={baseTheme}
onPointerUpdate={handleCursorUpdate}
externalPointerEvents={{
tooltip: { visible: true },
}}
onBrushEnd={(brush) => {
const { from, to } = getBrushData(brush);
updateInvestigationParams({
timeRange: { from, to },
});
}}
/>
<Axis id="y" position={Position.Left} hide />
<Axis
id="x"
position={Position.Bottom}
tickFormat={(d) => moment(d).format('LTS')}
style={{
tickLine: {
visible: true,
strokeWidth: 1,
stroke: '#98A2B3',
},
}}
/>
{alertEvents?.map((event) => (
<AlertEvent key={event.id} event={event} />
))}
{annotations?.map((annotation) => (
<AnnotationEvent key={annotation.id} event={annotation} />
))}
<AreaSeries
id="Time"
xScaleType={ScaleType.Time}
xAccessor="x"
yAccessors={['y']}
data={data}
filterSeriesInTooltip={() => false}
/>
</Chart>
</>
);
};

View file

@ -0,0 +1,47 @@
/*
* 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 { PartialTheme } from '@elastic/charts';
export const TIME_LINE_THEME: PartialTheme = {
highlighter: {
point: {
opacity: 0,
},
},
axes: {
gridLine: {
horizontal: {
visible: false,
},
vertical: {
visible: false,
},
},
axisLine: {
strokeWidth: 1,
stroke: '#98A2B3',
},
},
chartMargins: {
bottom: 10,
top: 10,
},
areaSeriesStyle: {
area: {
visible: false,
},
line: {
visible: false,
},
},
lineAnnotation: {
line: {
opacity: 0,
},
},
};

View file

@ -6,8 +6,9 @@
*/
import datemath from '@elastic/datemath';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { EventsTimeLine } from '../events_timeline/events_timeline';
import { useInvestigation } from '../../contexts/investigation_context';
import { AddInvestigationItem } from '../add_investigation_item/add_investigation_item';
import { InvestigationItemsList } from '../investigation_items_list/investigation_items_list';
@ -18,8 +19,8 @@ export function InvestigationItems() {
const { globalParams, updateInvestigationParams, investigation } = useInvestigation();
return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<>
<EuiFlexGroup direction="column" gutterSize="s">
<InvestigationSearchBar
dateRangeFrom={globalParams.timeRange.from}
dateRangeTo={globalParams.timeRange.to}
@ -32,17 +33,24 @@ export function InvestigationItems() {
updateInvestigationParams({ timeRange: nextTimeRange });
}}
/>
</EuiFlexItem>
{investigation?.id && (
<EuiFlexItem grow={false}>
<AssistantHypothesis investigationId={investigation.id} />
<EventsTimeLine />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<InvestigationItemsList />
</EuiFlexItem>
{investigation?.id && (
<EuiFlexItem grow={false}>
<AssistantHypothesis investigationId={investigation.id} />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<InvestigationItemsList />
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<AddInvestigationItem />
</EuiFlexGroup>
</>
);
}

View file

@ -86,11 +86,13 @@ export class InvestigateAppPlugin
]);
const services: InvestigateAppServices = {
...coreStart,
esql: createEsqlService({
data: pluginsStart.data,
dataViews: pluginsStart.dataViews,
lens: pluginsStart.lens,
}),
charts: pluginsStart.charts,
};
ReactDOM.render(
@ -130,6 +132,7 @@ export class InvestigateAppPlugin
dataViews: pluginsStart.dataViews,
lens: pluginsStart.lens,
}),
charts: pluginsStart.charts,
},
});
});

View file

@ -5,8 +5,10 @@
* 2.0.
*/
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { EsqlService } from './esql';
export interface InvestigateAppServices {
esql: EsqlService;
charts: ChartsPluginStart;
}

View file

@ -8,6 +8,7 @@ import type {
ObservabilityAIAssistantPublicSetup,
ObservabilityAIAssistantPublicStart,
} from '@kbn/observability-ai-assistant-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import type {
@ -66,6 +67,7 @@ export interface InvestigateAppStartDependencies {
unifiedSearch: UnifiedSearchPublicPluginStart;
uiActions: UiActionsStart;
security: SecurityPluginStart;
charts: ChartsPluginStart;
}
export interface InvestigateAppPublicSetup {}

View file

@ -66,6 +66,7 @@
"@kbn/core-elasticsearch-server",
"@kbn/calculate-auto",
"@kbn/ml-random-sampler-utils",
"@kbn/charts-plugin",
"@kbn/observability-utils",
],
}