generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: traces page and basic setup (#2764)
- Loading branch information
1 parent
2b4c51f
commit 915f07d
Showing
12 changed files
with
360 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
60
frontend/console/src/features/traces/details/TraceDetailsCall.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
) | ||
} |
Oops, something went wrong.