Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RCA] Events timeline !! #193265

Merged
merged 12 commits into from
Oct 7, 2024
3 changes: 2 additions & 1 deletion packages/kbn-investigation-shared/src/rest_specs/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
16 changes: 16 additions & 0 deletions x-pack/packages/observability/observability_utils/chart/utils.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"observabilityAIAssistant",
"observabilityShared",
"lens",
"charts",
"dataViews",
"data",
"embeddable",
Expand All @@ -26,7 +27,7 @@
"requiredBundles": [
"esql",
"kibanaReact",
"kibanaUtils",
"kibanaUtils"
],
"optionalPlugins": [],
"extraPublicDirs": []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const investigationKeys = {
userProfiles: (profileIds: Set<string>) =>
[...investigationKeys.all, 'userProfiles', ...profileIds] as const,
tags: () => [...investigationKeys.all, 'tags'] as const,
events: () => [...investigationKeys.all, 'events'] as const,
stats: () => [...investigationKeys.all, 'stats'] as const,
lists: () => [...investigationKeys.all, 'list'] as const,
list: (params: { page: number; perPage: number; search?: string; filter?: string }) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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(),
queryFn: async ({ signal }) => {
return await http.get<GetEventsResponse>(`/api/observability/events`, {
query: {
rangeFrom,
rangeTo,
},
version: '2023-10-31',
signal,
});
},
cacheTime: 600 * 1000, // 10_minutes
benakansara marked this conversation as resolved.
Show resolved Hide resolved
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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 { InvestigateTimeLine } from '../investigation_timeline/investigation_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';
Expand All @@ -17,8 +18,8 @@ export function InvestigationItems() {
const { globalParams, updateInvestigationParams } = useInvestigation();

return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<>
<EuiFlexGroup direction="column" gutterSize="s">
<InvestigationSearchBar
dateRangeFrom={globalParams.timeRange.from}
dateRangeTo={globalParams.timeRange.to}
Expand All @@ -31,13 +32,19 @@ export function InvestigationItems() {
updateInvestigationParams({ timeRange: nextTimeRange });
}}
/>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<InvestigationItemsList />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<InvestigateTimeLine />
</EuiFlexItem>

<EuiFlexItem grow={false}>
<InvestigationItemsList />
</EuiFlexItem>
</EuiFlexGroup>

<EuiSpacer size="m" />

<AddInvestigationItem />
</EuiFlexGroup>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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 } from '@elastic/charts';
import { EuiIcon } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { EventSchema } from '@kbn/investigation-shared';

export const AlertEvent = ({ event }: { event: EventSchema }) => {
return (
<LineAnnotation
id={event.id}
domainType="xDomain"
marker={
<>
<div
style={{
marginTop: -10,
backgroundColor: 'red',
borderRadius: 4,
padding: 4,
display: 'flex',
height: 20,
width: 20,
}}
>
<EuiIcon type="warning" size="s" color="white" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we wanted to keep "dot" icon with red color for alert and "dot" icon with black color for annotation events

</div>
<span
style={{
position: 'relative',
top: -40,
}}
>
{ALERT_LABEL}
benakansara marked this conversation as resolved.
Show resolved Hide resolved
</span>
</>
}
markerPosition="bottom"
dataValues={[
{
dataValue: moment(event.timestamp).valueOf(),
header: moment(event.timestamp).format('lll'),
details: event.description,
},
]}
/>
);
};

const ALERT_LABEL = i18n.translate('xpack.investigateApp.alertEvent.alertLabel', {
defaultMessage: 'Alert',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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>
}
// markerBody={
benakansara marked this conversation as resolved.
Show resolved Hide resolved
// // <EuiText style={{ marginTop: -45, position: 'relative' }}>{event.description}</EuiText>
// }
markerPosition="bottom"
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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, { 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 InvestigateTimeLine = () => {
benakansara marked this conversation as resolved.
Show resolved Hide resolved
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,
});

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={[
{ x: moment(globalParams.timeRange.from).valueOf(), y: 0 },
{ x: moment(globalParams.timeRange.to).valueOf(), y: 0 },
]}
/>
</Chart>
</>
);
};
Loading