From e4b9646a5f99d1e871ab257aaebdef642317bf51 Mon Sep 17 00:00:00 2001 From: Wes Date: Fri, 20 Sep 2024 14:31:57 -0700 Subject: [PATCH] feat: make trace graph generic and show ingree events (#2761) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2734 We can now show ingress and call events in the trace graph. We can iterate on styling etc. in the future, but this at least gets the events in. Next up will be a trace specific list and page to navigate around traces and drill into events. ![Screenshot 2024-09-20 at 1 06 27 PM](https://github.com/user-attachments/assets/88657349-995f-41b1-a001-8770ed482143) ![Screenshot 2024-09-20 at 12 59 43 PM](https://github.com/user-attachments/assets/5bef61d1-0e8e-4d45-9208-e10578b52d27) ![Screenshot 2024-09-20 at 12 59 57 PM](https://github.com/user-attachments/assets/a8655792-51c3-49a3-abc1-308fc91ce491) --- backend/controller/console/console.go | 2 + backend/controller/timeline/timeline.go | 1 + .../api/timeline/use-request-trace-events.ts | 17 +++ .../src/api/timeline/use-timeline-calls.ts | 6 +- .../console/src/features/calls/CallList.tsx | 82 +++++++------- .../src/features/requests/RequestGraph.tsx | 96 ----------------- .../src/features/timeline/Timeline.tsx | 4 +- .../timeline/details/TimelineCallDetails.tsx | 46 ++++---- .../details/TimelineIngressDetails.tsx | 51 ++++----- .../src/features/traces/TraceGraph.tsx | 101 ++++++++++++++++++ .../console/src/features/verbs/VerbPage.tsx | 4 +- 11 files changed, 217 insertions(+), 193 deletions(-) create mode 100644 frontend/console/src/api/timeline/use-request-trace-events.ts delete mode 100644 frontend/console/src/features/requests/RequestGraph.tsx create mode 100644 frontend/console/src/features/traces/TraceGraph.tsx diff --git a/backend/controller/console/console.go b/backend/controller/console/console.go index af368e131b..054cfc7c62 100644 --- a/backend/controller/console/console.go +++ b/backend/controller/console/console.go @@ -310,6 +310,8 @@ func eventsQueryProtoToDAL(pb *pbconsole.EventsQuery) ([]timeline.TimelineFilter eventTypes = append(eventTypes, timeline.EventTypeDeploymentCreated) case pbconsole.EventType_EVENT_TYPE_DEPLOYMENT_UPDATED: eventTypes = append(eventTypes, timeline.EventTypeDeploymentUpdated) + case pbconsole.EventType_EVENT_TYPE_INGRESS: + eventTypes = append(eventTypes, timeline.EventTypeIngress) default: return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("unknown event type %v", eventType)) } diff --git a/backend/controller/timeline/timeline.go b/backend/controller/timeline/timeline.go index 0fa9715082..a3cf3924e8 100644 --- a/backend/controller/timeline/timeline.go +++ b/backend/controller/timeline/timeline.go @@ -18,6 +18,7 @@ const ( EventTypeCall = sql.EventTypeCall EventTypeDeploymentCreated = sql.EventTypeDeploymentCreated EventTypeDeploymentUpdated = sql.EventTypeDeploymentUpdated + EventTypeIngress = sql.EventTypeIngress ) // TimelineEvent types. diff --git a/frontend/console/src/api/timeline/use-request-trace-events.ts b/frontend/console/src/api/timeline/use-request-trace-events.ts new file mode 100644 index 0000000000..8489e6768a --- /dev/null +++ b/frontend/console/src/api/timeline/use-request-trace-events.ts @@ -0,0 +1,17 @@ +import { type CallEvent, EventType, type EventsQuery_Filter, type IngressEvent } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts' +import { eventTypesFilter, requestKeysFilter } from './timeline-filters.ts' +import { useTimeline } from './use-timeline.ts' + +export type TraceEvent = CallEvent | IngressEvent + +export const useRequestTraceEvents = (requestKey?: string, filters: EventsQuery_Filter[] = []) => { + const eventTypes = [EventType.CALL, EventType.INGRESS] + const allFilters = [...filters, requestKeysFilter([requestKey || '']), eventTypesFilter(eventTypes)] + const timelineQuery = useTimeline(true, allFilters, !!requestKey) + + const data = timelineQuery.data?.filter((event) => event.entry.case === 'call' || event.entry.case === 'ingress') ?? [] + return { + ...timelineQuery, + data, + } +} diff --git a/frontend/console/src/api/timeline/use-timeline-calls.ts b/frontend/console/src/api/timeline/use-timeline-calls.ts index 870d9a859a..fa1af09531 100644 --- a/frontend/console/src/api/timeline/use-timeline-calls.ts +++ b/frontend/console/src/api/timeline/use-timeline-calls.ts @@ -1,4 +1,4 @@ -import { type CallEvent, EventType, type EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts' +import { EventType, type EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts' import { eventTypesFilter } from './timeline-filters.ts' import { useTimeline } from './use-timeline.ts' @@ -6,9 +6,7 @@ export const useTimelineCalls = (isStreaming: boolean, filters: EventsQuery_Filt const allFilters = [...filters, eventTypesFilter([EventType.CALL])] const timelineQuery = useTimeline(isStreaming, allFilters, enabled) - // Map the events to CallEvent for ease of use - const data = timelineQuery.data?.map((event) => event.entry.value as CallEvent) || [] - + const data = timelineQuery.data || [] return { ...timelineQuery, data, diff --git a/frontend/console/src/features/calls/CallList.tsx b/frontend/console/src/features/calls/CallList.tsx index 78d2ddce2c..56a7900008 100644 --- a/frontend/console/src/features/calls/CallList.tsx +++ b/frontend/console/src/features/calls/CallList.tsx @@ -1,22 +1,23 @@ import { useContext, useState } from 'react' -import type { CallEvent } from '../../protos/xyz/block/ftl/v1/console/console_pb' +import type { CallEvent, Event } from '../../protos/xyz/block/ftl/v1/console/console_pb' import { SidePanelContext } from '../../providers/side-panel-provider' import { formatDuration, formatTimestampShort } from '../../utils' import { TimelineCallDetails } from '../timeline/details/TimelineCallDetails' import { verbRefString } from '../verbs/verb.utils' -export const CallList = ({ calls }: { calls: CallEvent[] | undefined }) => { +export const CallList = ({ calls }: { calls: Event[] | undefined }) => { const { openPanel, closePanel } = useContext(SidePanelContext) - const [selectedCall, setSelectedCall] = useState() + const [selectedCallId, setSelectedCallId] = useState() - const handleCallClicked = (call: CallEvent) => { - if (selectedCall?.equals(call)) { - setSelectedCall(undefined) + const handleCallClicked = (event: Event) => { + if (selectedCallId === event.id) { + setSelectedCallId(undefined) closePanel() return } - setSelectedCall(call) - openPanel() + setSelectedCallId(event.id) + const call = event.entry.value as CallEvent + openPanel() } return ( @@ -38,38 +39,41 @@ export const CallList = ({ calls }: { calls: CallEvent[] | undefined }) => {
- {calls?.map((call, index) => ( - handleCallClicked(call)} - > - - - handleCallClicked(callEvent)} > - {call.sourceVerbRef && verbRefString(call.sourceVerbRef)} - - - - - - - ))} + + + + + + + + + ) + })}
{formatTimestampShort(call.timeStamp)}{formatDuration(call.duration)} { + const call = callEvent.entry.value as CallEvent + return ( +
- {call.destinationVerbRef && verbRefString(call.destinationVerbRef)} - - {call.request} - - {call.response} - - {call.error} -
{formatTimestampShort(callEvent.timeStamp)}{formatDuration(call.duration)} + {call.sourceVerbRef && verbRefString(call.sourceVerbRef)} + + {call.destinationVerbRef && verbRefString(call.destinationVerbRef)} + + {call.request} + + {call.response} + + {call.error} +
diff --git a/frontend/console/src/features/requests/RequestGraph.tsx b/frontend/console/src/features/requests/RequestGraph.tsx deleted file mode 100644 index f2736a2886..0000000000 --- a/frontend/console/src/features/requests/RequestGraph.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import type { Duration, Timestamp } from '@bufbuild/protobuf' -import { useRequestCalls } from '../../api/timeline/use-request-calls' -import { Loader } from '../../components/Loader' -import type { CallEvent } from '../../protos/xyz/block/ftl/v1/console/console_pb' -import { verbRefString } from '../verbs/verb.utils' - -const CallBlock = ({ - call, - selectedCall, - firstTimeStamp, - firstDuration, -}: { - call: CallEvent - selectedCall?: CallEvent - firstTimeStamp: Timestamp - firstDuration: Duration -}) => { - const totalDurationMillis = (firstDuration.nanos ?? 0) / 1000000 - const durationInMillis = (call.duration?.nanos ?? 0) / 1000000 - let width = (durationInMillis / totalDurationMillis) * 100 - if (width < 1) { - width = 1 - } - - const callTime = call.timeStamp?.toDate() ?? new Date() - const initialTime = firstTimeStamp?.toDate() ?? new Date() - const offsetInMillis = callTime.getTime() - initialTime.getTime() - const leftOffsetPercentage = (offsetInMillis / totalDurationMillis) * 100 - - const barColor = call.equals(selectedCall) ? 'bg-green-500' : 'bg-indigo-500' - - return ( -
-
-
-
- -
{`${durationInMillis}ms`}
- {call.destinationVerbRef && ( - - {verbRefString(call.destinationVerbRef)} - - )} -
- ) -} - -interface Props { - call?: CallEvent - setSelectedCall: React.Dispatch> -} - -export const RequestGraph = ({ call, setSelectedCall }: Props) => { - const requestCalls = useRequestCalls(call?.requestKey) - - if (requestCalls.isLoading) { - return ( -
- -
- ) - } - - const calls = requestCalls.data?.reverse() || [] - - if (calls.length === 0) { - return <> - } - - const firstTimeStamp = calls[0].timeStamp - const firstDuration = calls[0].duration - if (firstTimeStamp === undefined || firstDuration === undefined) { - return <> - } - - return ( -
- {calls.map((c, index) => ( -
setSelectedCall(c)}> -
- -
-
- ))} -
- ) -} diff --git a/frontend/console/src/features/timeline/Timeline.tsx b/frontend/console/src/features/timeline/Timeline.tsx index e6141309c9..59709988a8 100644 --- a/frontend/console/src/features/timeline/Timeline.tsx +++ b/frontend/console/src/features/timeline/Timeline.tsx @@ -56,7 +56,7 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings switch (entry.entry?.case) { case 'call': - openPanel(, handlePanelClosed) + openPanel(, handlePanelClosed) break case 'log': openPanel(, handlePanelClosed) @@ -68,7 +68,7 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings openPanel(, handlePanelClosed) break case 'ingress': - openPanel(, handlePanelClosed) + openPanel(, handlePanelClosed) break default: break diff --git a/frontend/console/src/features/timeline/details/TimelineCallDetails.tsx b/frontend/console/src/features/timeline/details/TimelineCallDetails.tsx index 7f43dd4c2b..ca731380f4 100644 --- a/frontend/console/src/features/timeline/details/TimelineCallDetails.tsx +++ b/frontend/console/src/features/timeline/details/TimelineCallDetails.tsx @@ -1,24 +1,20 @@ import type { Timestamp } from '@bufbuild/protobuf' -import { useContext, useEffect, useState } from 'react' +import { useContext } from 'react' import { AttributeBadge } from '../../../components/AttributeBadge' import { CloseButton } from '../../../components/CloseButton' import { CodeBlock } from '../../../components/CodeBlock' -import type { CallEvent } from '../../../protos/xyz/block/ftl/v1/console/console_pb' +import type { CallEvent, Event } from '../../../protos/xyz/block/ftl/v1/console/console_pb' import { SidePanelContext } from '../../../providers/side-panel-provider' import { formatDuration } from '../../../utils/date.utils' import { DeploymentCard } from '../../deployments/DeploymentCard' -import { RequestGraph } from '../../requests/RequestGraph' +import { TraceGraph } from '../../traces/TraceGraph' import { verbRefString } from '../../verbs/verb.utils' import { TimelineTimestamp } from './TimelineTimestamp' -export const TimelineCallDetails = ({ timestamp, call }: { timestamp?: Timestamp; call: CallEvent }) => { +export const TimelineCallDetails = ({ timestamp, event }: { timestamp?: Timestamp; event: Event }) => { const { closePanel } = useContext(SidePanelContext) - const [selectedCall, setSelectedCall] = useState(call) - - useEffect(() => { - setSelectedCall(call) - }, [call]) + const call = event.entry.value as CallEvent return (
@@ -35,28 +31,28 @@ export const TimelineCallDetails = ({ timestamp, call }: { timestamp?: Timestamp
-
- +
+
Request
- + - {selectedCall.response !== 'null' && ( + {call.response !== 'null' && ( <>
Response
- + )} - {selectedCall.error && ( + {call.error && ( <>

Error

- - {selectedCall.stack && ( + + {call.stack && ( <>

Stack

- + )} @@ -65,22 +61,22 @@ export const TimelineCallDetails = ({ timestamp, call }: { timestamp?: Timestamp
    - {selectedCall.requestKey && ( + {call.requestKey && (
  • - +
  • )}
  • - +
  • - {selectedCall.destinationVerbRef && ( + {call.destinationVerbRef && (
  • - +
  • )} - {selectedCall.sourceVerbRef && ( + {call.sourceVerbRef && (
  • - +
  • )}
diff --git a/frontend/console/src/features/timeline/details/TimelineIngressDetails.tsx b/frontend/console/src/features/timeline/details/TimelineIngressDetails.tsx index c8a04bf31d..c2c09bf8e3 100644 --- a/frontend/console/src/features/timeline/details/TimelineIngressDetails.tsx +++ b/frontend/console/src/features/timeline/details/TimelineIngressDetails.tsx @@ -1,23 +1,20 @@ import type { Timestamp } from '@bufbuild/protobuf' -import { useContext, useEffect, useState } from 'react' +import { useContext } from 'react' import { AttributeBadge } from '../../../components/AttributeBadge' import { CloseButton } from '../../../components/CloseButton' import { CodeBlock } from '../../../components/CodeBlock' -import type { IngressEvent } from '../../../protos/xyz/block/ftl/v1/console/console_pb' +import type { Event, IngressEvent } from '../../../protos/xyz/block/ftl/v1/console/console_pb' import { SidePanelContext } from '../../../providers/side-panel-provider' import { formatDuration } from '../../../utils/date.utils' import { DeploymentCard } from '../../deployments/DeploymentCard' +import { TraceGraph } from '../../traces/TraceGraph' import { verbRefString } from '../../verbs/verb.utils' import { TimelineTimestamp } from './TimelineTimestamp' -export const TimelineIngressDetails = ({ timestamp, ingress }: { timestamp?: Timestamp; ingress: IngressEvent }) => { +export const TimelineIngressDetails = ({ timestamp, event }: { timestamp?: Timestamp; event: Event }) => { const { closePanel } = useContext(SidePanelContext) - const [selectedIngress, setSelectedIngress] = useState(ingress) - - useEffect(() => { - setSelectedIngress(ingress) - }, [ingress]) + const ingress = event.entry.value as IngressEvent return (
@@ -34,34 +31,38 @@ export const TimelineIngressDetails = ({ timestamp, ingress }: { timestamp?: Tim
+
+ +
+
Request
- + - {selectedIngress.response !== 'null' && ( + {ingress.response !== 'null' && ( <>
Response
- + )} - {selectedIngress.requestHeader !== 'null' && ( + {ingress.requestHeader !== 'null' && ( <>
Request Header
- + )} - {selectedIngress.responseHeader !== 'null' && ( + {ingress.responseHeader !== 'null' && ( <>
Response Header
- + )} - {selectedIngress.error && ( + {ingress.error && ( <>

Error

- + )} @@ -69,25 +70,25 @@ export const TimelineIngressDetails = ({ timestamp, ingress }: { timestamp?: Tim
  • - +
  • - +
  • - +
  • - {selectedIngress.requestKey && ( + {ingress.requestKey && (
  • - +
  • )}
  • - +
  • - {selectedIngress.verbRef && ( + {ingress.verbRef && (
  • - +
  • )}
diff --git a/frontend/console/src/features/traces/TraceGraph.tsx b/frontend/console/src/features/traces/TraceGraph.tsx new file mode 100644 index 0000000000..2509223b75 --- /dev/null +++ b/frontend/console/src/features/traces/TraceGraph.tsx @@ -0,0 +1,101 @@ +import type { Duration, Timestamp } from '@bufbuild/protobuf' +import { useState } from 'react' +import { type TraceEvent, useRequestTraceEvents } from '../../api/timeline/use-request-trace-events' +import { CallEvent, IngressEvent } from '../../protos/xyz/block/ftl/v1/console/console_pb' + +const EventBlock = ({ + event, + isSelected, + firstTimeStamp, + firstDuration, +}: { + event: TraceEvent + isSelected: boolean + firstTimeStamp: Timestamp + firstDuration: Duration +}) => { + const [isHovering, setIsHovering] = useState(false) + + const totalDurationMillis = (firstDuration.nanos ?? 0) / 1000000 + const durationInMillis = (event.duration?.nanos ?? 0) / 1000000 + let width = (durationInMillis / totalDurationMillis) * 100 + if (width < 1) { + width = 1 + } + + const callTime = event.timeStamp?.toDate() ?? new Date() + const initialTime = firstTimeStamp?.toDate() ?? new Date() + const offsetInMillis = callTime.getTime() - initialTime.getTime() + const leftOffsetPercentage = (offsetInMillis / totalDurationMillis) * 100 + + let barColor = 'bg-pink-500' + let eventTarget = '' + + if (event instanceof CallEvent) { + barColor = 'bg-indigo-500' + eventTarget = `${event.destinationVerbRef?.module}.${event.destinationVerbRef?.name}` + } else if (event instanceof IngressEvent) { + barColor = 'bg-blue-500' + eventTarget = event.path + } + + if (isSelected) { + barColor = 'bg-pink-500' + } + + return ( +
setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}> +
+
+ {isHovering && ( +
+

+ {event instanceof CallEvent ? 'Call ' : 'Ingress '} + {eventTarget} + {` (${durationInMillis} ms)`} +

+
+ )} +
+
+ ) +} + +export const TraceGraph = ({ requestKey, selectedEventId }: { requestKey?: string; selectedEventId?: bigint }) => { + const requestEvents = useRequestTraceEvents(requestKey) + const events = requestEvents.data?.reverse() ?? [] + + if (events.length === 0) { + return + } + + const firstTimeStamp = events[0].timeStamp + const traceEvent = events[0].entry.value as TraceEvent + const firstDuration = traceEvent.duration + if (firstTimeStamp === undefined || firstDuration === undefined) { + return + } + + return ( +
+ {events.map((c, index) => ( +
+
+ +
+
+ ))} +
+ ) +} diff --git a/frontend/console/src/features/verbs/VerbPage.tsx b/frontend/console/src/features/verbs/VerbPage.tsx index 09aaf1d9fd..da8f4d4146 100644 --- a/frontend/console/src/features/verbs/VerbPage.tsx +++ b/frontend/console/src/features/verbs/VerbPage.tsx @@ -6,7 +6,7 @@ import { useStreamVerbCalls } from '../../api/timeline/stream-verb-calls' import { Loader } from '../../components/Loader' import { ResizablePanels } from '../../components/ResizablePanels' import { Page } from '../../layout' -import type { CallEvent, Module, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' +import type { Module, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' import { NotificationType, NotificationsContext } from '../../providers/notifications-provider' import { SidePanelProvider } from '../../providers/side-panel-provider' import { CallList } from '../calls/CallList' @@ -40,7 +40,7 @@ export const VerbPage = ({ moduleName, declName }: { moduleName: string; declNam }, [modules.data, moduleName]) const callEvents = useStreamVerbCalls(module?.name, verb?.verb?.name) - const calls: CallEvent[] = callEvents.data || [] + const calls = callEvents.data || [] if (!module || !verb) { return (