Skip to content

Commit

Permalink
feat: traces page and basic setup (#2764)
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman authored Sep 23, 2024
1 parent 2b4c51f commit 915f07d
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 15 deletions.
3 changes: 1 addition & 2 deletions frontend/console/src/features/calls/CallList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export const CallList = ({ calls }: { calls: Event[] | undefined }) => {
return
}
setSelectedCallId(event.id)
const call = event.entry.value as CallEvent
openPanel(<TimelineCallDetails timestamp={call.timeStamp} event={event} />)
openPanel(<TimelineCallDetails event={event} />)
}

return (
Expand Down
5 changes: 2 additions & 3 deletions frontend/console/src/features/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Timestamp } from '@bufbuild/protobuf'
import { useContext, useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { timeFilter, useTimeline } from '../../api/timeline/index.ts'
Expand Down Expand Up @@ -56,7 +55,7 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings

switch (entry.entry?.case) {
case 'call':
openPanel(<TimelineCallDetails timestamp={entry.timeStamp as Timestamp} event={entry} />, handlePanelClosed)
openPanel(<TimelineCallDetails event={entry} />, handlePanelClosed)
break
case 'log':
openPanel(<TimelineLogDetails event={entry} log={entry.entry.value} />, handlePanelClosed)
Expand All @@ -68,7 +67,7 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings
openPanel(<TimelineDeploymentUpdatedDetails event={entry} deployment={entry.entry.value} />, handlePanelClosed)
break
case 'ingress':
openPanel(<TimelineIngressDetails timestamp={entry.timeStamp as Timestamp} event={entry} />, handlePanelClosed)
openPanel(<TimelineIngressDetails event={entry} />, handlePanelClosed)
break
default:
break
Expand Down
2 changes: 1 addition & 1 deletion frontend/console/src/features/timeline/TimelinePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const TimelinePage = () => {
useEffect(() => {
if (initialEventId) {
// if we're loading a specific event, we don't want to tail.
setSelectedTimeRange(TIME_RANGES['5m'])
setSelectedTimeRange(TIME_RANGES['24h'])
setIsTimelinePaused(true)
}
}, [])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Timestamp } from '@bufbuild/protobuf'
import { useContext } from 'react'
import { AttributeBadge } from '../../../components/AttributeBadge'
import { CloseButton } from '../../../components/CloseButton'
Expand All @@ -8,10 +7,11 @@ import { SidePanelContext } from '../../../providers/side-panel-provider'
import { formatDuration } from '../../../utils/date.utils'
import { DeploymentCard } from '../../deployments/DeploymentCard'
import { TraceGraph } from '../../traces/TraceGraph'
import { TraceGraphHeader } from '../../traces/TraceGraphHeader'
import { verbRefString } from '../../verbs/verb.utils'
import { TimelineTimestamp } from './TimelineTimestamp'

export const TimelineCallDetails = ({ timestamp, event }: { timestamp?: Timestamp; event: Event }) => {
export const TimelineCallDetails = ({ event }: { event: Event }) => {
const { closePanel } = useContext(SidePanelContext)

const call = event.entry.value as CallEvent
Expand All @@ -26,12 +26,13 @@ export const TimelineCallDetails = ({ timestamp, event }: { timestamp?: Timestam
</div>
)}
</div>
<TimelineTimestamp timestamp={timestamp} />
<TimelineTimestamp timestamp={event.timeStamp} />
</div>
<CloseButton onClick={closePanel} />
</div>

<div className='mt-4'>
<div className='mt-2'>
<TraceGraphHeader requestKey={call.requestKey} eventId={event.id} />
<TraceGraph requestKey={call.requestKey} selectedEventId={event.id} />
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Timestamp } from '@bufbuild/protobuf'
import { useContext } from 'react'
import { AttributeBadge } from '../../../components/AttributeBadge'
import { CloseButton } from '../../../components/CloseButton'
Expand All @@ -8,10 +7,11 @@ import { SidePanelContext } from '../../../providers/side-panel-provider'
import { formatDuration } from '../../../utils/date.utils'
import { DeploymentCard } from '../../deployments/DeploymentCard'
import { TraceGraph } from '../../traces/TraceGraph'
import { TraceGraphHeader } from '../../traces/TraceGraphHeader'
import { verbRefString } from '../../verbs/verb.utils'
import { TimelineTimestamp } from './TimelineTimestamp'

export const TimelineIngressDetails = ({ timestamp, event }: { timestamp?: Timestamp; event: Event }) => {
export const TimelineIngressDetails = ({ event }: { event: Event }) => {
const { closePanel } = useContext(SidePanelContext)

const ingress = event.entry.value as IngressEvent
Expand All @@ -26,12 +26,13 @@ export const TimelineIngressDetails = ({ timestamp, event }: { timestamp?: Times
</div>
)}
</div>
<TimelineTimestamp timestamp={timestamp} />
<TimelineTimestamp timestamp={event.timeStamp} />
</div>
<CloseButton onClick={closePanel} />
</div>

<div className='mt-4'>
<div className='mt-2'>
<TraceGraphHeader requestKey={ingress.requestKey} eventId={event.id} />
<TraceGraph requestKey={ingress.requestKey} selectedEventId={event.id} />
</div>

Expand Down
97 changes: 97 additions & 0 deletions frontend/console/src/features/traces/TraceDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type React from 'react'
import { useNavigate } from 'react-router-dom'
import type { TraceEvent } from '../../api/timeline/use-request-trace-events'
import { CallEvent, type Event, IngressEvent } from '../../protos/xyz/block/ftl/v1/console/console_pb'
import { TimelineIcon } from '../timeline/TimelineIcon'

interface TraceDetailsProps {
requestKey: string
events: Event[]
selectedEventId?: bigint
}

export const TraceDetails: React.FC<TraceDetailsProps> = ({ events, selectedEventId, requestKey }) => {
const navigate = useNavigate()

const firstTimeStamp = events[0]?.timeStamp
const firstEvent = events[0].entry.value as TraceEvent
const firstDuration = firstEvent?.duration
const totalDurationMillis = (firstDuration?.nanos ?? 0) / 1000000

const handleEventClick = (eventId: bigint) => {
navigate(`/traces/${requestKey}?event_id=${eventId}`)
}

return (
<div>
<div className='mb-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg shadow-sm'>
<h2 className='font-semibold text-lg text-gray-800 dark:text-gray-100 mb-2'>
Total Duration: <span className='font-bold text-indigo-600 dark:text-indigo-400'>{totalDurationMillis} ms</span>
</h2>
<p className='text-sm text-gray-600 dark:text-gray-300'>
Start Time: <span className='text-gray-800 dark:text-gray-100'>{firstTimeStamp?.toDate().toLocaleString()}</span>
</p>
</div>

<ul className='space-y-2'>
{events.map((event, index) => {
const traceEvent = event.entry.value as TraceEvent
const durationInMillis = (traceEvent.duration?.nanos ?? 0) / 1000000

let width = (durationInMillis / totalDurationMillis) * 100
if (width < 1) width = 1

const callTime = traceEvent.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 action = ''
let eventName = ''
const icon = <TimelineIcon event={event} />

if (traceEvent instanceof CallEvent) {
barColor = 'bg-indigo-500'
action = 'Call'
eventName = `${traceEvent.destinationVerbRef?.module}.${traceEvent.destinationVerbRef?.name}`
} else if (traceEvent instanceof IngressEvent) {
barColor = 'bg-yellow-500'
action = `HTTP ${traceEvent.method}`
eventName = `${traceEvent.path}`
}

if (event.id === selectedEventId) {
barColor = 'bg-pink-500'
}

const isSelected = event.id === selectedEventId
const listItemClass = isSelected
? 'flex items-center justify-between p-2 bg-indigo-100/50 dark:bg-indigo-700 rounded cursor-pointer'
: 'flex items-center justify-between p-2 hover:bg-indigo-500/10 rounded cursor-pointer'

return (
<li key={index} className={listItemClass} onClick={() => handleEventClick(event.id)}>
<span className='flex items-center w-1/2 text-sm font-medium'>
<span className='mr-2'>{icon}</span>
<span className='mr-2'>{action}</span>
{eventName}
</span>

<div className='relative w-2/3 h-4 flex-grow'>
<div
className={`absolute h-4 ${barColor} rounded-sm`}
style={{
width: `${width}%`,
left: `${leftOffsetPercentage}%`,
}}
/>
</div>
<span className='text-xs font-medium ml-4 w-20 text-right'>{durationInMillis} ms</span>
</li>
)
})}
</ul>
</div>
)
}
2 changes: 1 addition & 1 deletion frontend/console/src/features/traces/TraceGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const EventBlock = ({
barColor = 'bg-indigo-500'
eventTarget = `${event.destinationVerbRef?.module}.${event.destinationVerbRef?.name}`
} else if (event instanceof IngressEvent) {
barColor = 'bg-blue-500'
barColor = 'bg-yellow-500'
eventTarget = event.path
}

Expand Down
39 changes: 39 additions & 0 deletions frontend/console/src/features/traces/TraceGraphHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Activity03Icon } from 'hugeicons-react'
import { useNavigate } from 'react-router-dom'
import { type TraceEvent, useRequestTraceEvents } from '../../api/timeline/use-request-trace-events'

export const TraceGraphHeader = ({ requestKey, eventId }: { requestKey?: string; eventId: bigint }) => {
const navigate = useNavigate()
const requestEvents = useRequestTraceEvents(requestKey)
const events = requestEvents.data?.reverse() ?? []

if (events.length === 0) {
return null
}

const firstTimeStamp = events[0].timeStamp
const traceEvent = events[0].entry.value as TraceEvent
const firstDuration = traceEvent.duration
if (firstTimeStamp === undefined || firstDuration === undefined) {
return null
}

const totalDurationMillis = (firstDuration.nanos ?? 0) / 1000000

return (
<div className='flex items-end justify-between'>
<span className='text-xs font-mono'>
Total <span>{totalDurationMillis}ms</span>
</span>

<button
type='button'
title='View trace'
onClick={() => navigate(`/traces/${requestKey}?event_id=${eventId}`)}
className='flex items-center p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer'
>
<Activity03Icon className='w-5 h-5' />
</button>
</div>
)
}
75 changes: 75 additions & 0 deletions frontend/console/src/features/traces/TracesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ArrowLeft02Icon } from 'hugeicons-react'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { useRequestTraceEvents } from '../../api/timeline/use-request-trace-events'
import { Loader } from '../../components/Loader'
import { TraceDetails } from './TraceDetails'
import { TraceDetailsCall } from './details/TraceDetailsCall'
import { TraceDetailsIngress } from './details/TraceDetailsIngress'

export const TracesPage = () => {
const navigate = useNavigate()

const { requestKey } = useParams<{ requestKey: string }>()
const requestEvents = useRequestTraceEvents(requestKey)
const events = requestEvents.data?.reverse() ?? []

const [searchParams] = useSearchParams()
const eventIdParam = searchParams.get('event_id')
const selectedEventId = eventIdParam ? BigInt(eventIdParam) : undefined

if (events.length === 0) {
return
}

if (requestKey === undefined) {
return
}

const handleBack = () => {
if (window.history.length > 1) {
navigate(-1)
} else {
navigate('/modules')
}
}

if (requestEvents.isLoading) {
return (
<div className='flex justify-center items-center min-h-screen'>
<Loader />
</div>
)
}

const selectedEvent = events.find((event) => event.id === selectedEventId)
let eventDetailsComponent: React.ReactNode
switch (selectedEvent?.entry.case) {
case 'call':
eventDetailsComponent = <TraceDetailsCall event={selectedEvent} />
break
case 'ingress':
eventDetailsComponent = <TraceDetailsIngress event={selectedEvent} />
break
default:
eventDetailsComponent = <p>No details available for this event type.</p>
break
}

return (
<div className='flex h-full'>
<div className='w-1/2 p-4 h-full overflow-y-auto'>
<div className='flex items-center mb-2'>
<button type='button' onClick={handleBack} className='flex items-center p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer'>
<ArrowLeft02Icon className='w-6 h-6' />
</button>
<span className='text-xl font-semibold ml-2'>Trace Details</span>
</div>
<TraceDetails requestKey={requestKey} events={events} selectedEventId={selectedEventId} />
</div>

<div className='my-4 border-l border-gray-100 dark:border-gray-700' />

<div className='w-1/2 p-4 mt-1 h-full overflow-y-auto'>{eventDetailsComponent}</div>
</div>
)
}
60 changes: 60 additions & 0 deletions frontend/console/src/features/traces/details/TraceDetailsCall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { AttributeBadge } from '../../../components/AttributeBadge'
import { CodeBlock } from '../../../components/CodeBlock'
import type { CallEvent, Event } from '../../../protos/xyz/block/ftl/v1/console/console_pb'
import { formatDuration } from '../../../utils/date.utils'
import { DeploymentCard } from '../../deployments/DeploymentCard'
import { verbRefString } from '../../verbs/verb.utils'

export const TraceDetailsCall = ({ event }: { event: Event }) => {
const call = event.entry.value as CallEvent
return (
<>
<span className='text-xl font-semibold'>Call Details</span>
<div className='text-sm pt-2'>Request</div>
<CodeBlock code={JSON.stringify(JSON.parse(call.request), null, 2)} language='json' />

{call.response !== 'null' && (
<>
<div className='text-sm pt-2'>Response</div>
<CodeBlock code={JSON.stringify(JSON.parse(call.response), null, 2)} language='json' />
</>
)}

{call.error && (
<>
<h3 className='pt-4'>Error</h3>
<CodeBlock code={call.error} language='text' />
{call.stack && (
<>
<h3 className='pt-4'>Stack</h3>
<CodeBlock code={call.stack} language='text' />
</>
)}
</>
)}

<DeploymentCard className='mt-4' deploymentKey={call.deploymentKey} />

<ul className='pt-4 space-y-2'>
{call.requestKey && (
<li>
<AttributeBadge name='Request' value={call.requestKey} />
</li>
)}
<li>
<AttributeBadge name='Duration' value={formatDuration(call.duration)} />
</li>
{call.destinationVerbRef && (
<li>
<AttributeBadge name='Destination' value={verbRefString(call.destinationVerbRef)} />
</li>
)}
{call.sourceVerbRef && (
<li>
<AttributeBadge name='Source' value={verbRefString(call.sourceVerbRef)} />
</li>
)}
</ul>
</>
)
}
Loading

0 comments on commit 915f07d

Please sign in to comment.