mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
2881b0423d
commit
89f2802505
15 changed files with 351 additions and 13 deletions
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
||||
}
|
|
@ -53,6 +53,7 @@ export function getMockInvestigateAppContext(): DeeplyMockedKeys<InvestigateAppK
|
|||
});
|
||||
}),
|
||||
},
|
||||
charts: {} as any,
|
||||
};
|
||||
|
||||
const core = coreMock.createStart();
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"investigate",
|
||||
"observabilityShared",
|
||||
"lens",
|
||||
"charts",
|
||||
"dataViews",
|
||||
"data",
|
||||
"embeddable",
|
||||
|
@ -25,7 +26,7 @@
|
|||
"requiredBundles": [
|
||||
"esql",
|
||||
"kibanaReact",
|
||||
"kibanaUtils",
|
||||
"kibanaUtils"
|
||||
],
|
||||
"optionalPlugins": ["observabilityAIAssistant"],
|
||||
"extraPublicDirs": []
|
||||
|
|
|
@ -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 }) =>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/calculate-auto",
|
||||
"@kbn/ml-random-sampler-utils",
|
||||
"@kbn/charts-plugin",
|
||||
"@kbn/observability-utils",
|
||||
],
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue