Skip to content

Commit

Permalink
feat: add shared event list and use in verb page (#2903)
Browse files Browse the repository at this point in the history
Fixes #2821

- Also adds new query type (modules) to timeline events.


https://github.com/user-attachments/assets/0676685a-4bb2-4a7a-b3d3-bdc3f9131c78

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
wesbillman and github-actions[bot] authored Sep 30, 2024
1 parent 51053ac commit 6aa5993
Show file tree
Hide file tree
Showing 19 changed files with 596 additions and 376 deletions.
6 changes: 6 additions & 0 deletions backend/controller/console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ func eventsQueryProtoToDAL(pb *pbconsole.EventsQuery) ([]timeline.TimelineFilter
destVerb = optional.Some(*filter.Call.DestVerb)
}
query = append(query, timeline.FilterCall(sourceModule, filter.Call.DestModule, destVerb))
case *pbconsole.EventsQuery_Filter_Module:
var verb optional.Option[string]
if filter.Module.Verb != nil {
verb = optional.Some(*filter.Module.Verb)
}
query = append(query, timeline.FilterModule(filter.Module.Module, verb))

default:
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("unknown filter %T", filter))
Expand Down
29 changes: 29 additions & 0 deletions backend/controller/timeline/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ type eventFilterCall struct {
destVerb optional.Option[string]
}

type eventFilterModule struct {
module string
verb optional.Option[string]
}

type eventFilter struct {
level *log.Level
calls []*eventFilterCall
module []*eventFilterModule
types []EventType
deployments []model.DeploymentKey
requests []string
Expand Down Expand Up @@ -58,6 +64,12 @@ func FilterCall(sourceModule optional.Option[string], destModule string, destVer
}
}

func FilterModule(module string, verb optional.Option[string]) TimelineFilter {
return func(query *eventFilter) {
query.module = append(query.module, &eventFilterModule{module: module, verb: verb})
}
}

func FilterDeployments(deploymentKeys ...model.DeploymentKey) TimelineFilter {
return func(query *eventFilter) {
query.deployments = append(query.deployments, deploymentKeys...)
Expand Down Expand Up @@ -205,6 +217,23 @@ func (s *Service) QueryTimeline(ctx context.Context, limit int, filters ...Timel
q += ")\n"
}

if len(filter.module) > 0 {
q += " AND ("
for i, module := range filter.module {
if i > 0 {
q += " OR "
}
if verb, ok := module.verb.Get(); ok {
q += fmt.Sprintf("((e.type = 'call' AND e.custom_key_3 = $%d AND e.custom_key_4 = $%d) OR (e.type = 'ingress' AND e.custom_key_1 = $%d AND e.custom_key_2 = $%d))",
param(module.module), param(verb), param(module.module), param(verb))
} else {
q += fmt.Sprintf("((e.type = 'call' AND e.custom_key_3 = $%d) OR (e.type = 'ingress' AND e.custom_key_1 = $%d))",
param(module.module), param(module.module))
}
}
q += ")\n"
}

if filter.descending {
q += " ORDER BY e.time_stamp DESC, e.id DESC"
} else {
Expand Down
14 changes: 13 additions & 1 deletion backend/controller/timeline/timeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestTimeline(t *testing.T) {
ingressEvent := &IngressEvent{
DeploymentKey: deploymentKey,
RequestKey: optional.Some(requestKey),
Verb: schema.Ref{},
Verb: schema.Ref{Module: "echo", Name: "echo"},
Method: "GET",
Path: "/echo",
StatusCode: 200,
Expand Down Expand Up @@ -169,6 +169,18 @@ func TestTimeline(t *testing.T) {
assertEventsEqual(t, []Event{callEvent}, events)
})

t.Run("ByModule", func(t *testing.T) {
events, err := timeline.QueryTimeline(ctx, 1000, FilterTypes(EventTypeIngress), FilterModule("echo", optional.None[string]()))
assert.NoError(t, err)
assertEventsEqual(t, []Event{ingressEvent}, events)
})

t.Run("ByModuleWithVerb", func(t *testing.T) {
events, err := timeline.QueryTimeline(ctx, 1000, FilterTypes(EventTypeIngress), FilterModule("echo", optional.Some("echo")))
assert.NoError(t, err)
assertEventsEqual(t, []Event{ingressEvent}, events)
})

t.Run("ByLogLevel", func(t *testing.T) {
events, err := timeline.QueryTimeline(ctx, 1000, FilterTypes(EventTypeLog), FilterLogLevel(log.Trace))
assert.NoError(t, err)
Expand Down
489 changes: 292 additions & 197 deletions backend/protos/xyz/block/ftl/v1/console/console.pb.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions backend/protos/xyz/block/ftl/v1/console/console.proto
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ message EventsQuery {
optional string dest_verb = 2;
optional string source_module = 3;
}
message ModuleFilter {
string module = 1;
optional string verb = 2;
}

enum Order {
ASC = 0;
Expand All @@ -211,6 +215,7 @@ message EventsQuery {
TimeFilter time = 6;
IDFilter id = 7;
CallFilter call = 8;
ModuleFilter module = 9;
}
}

Expand Down
15 changes: 14 additions & 1 deletion frontend/console/src/api/timeline/timeline-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
EventsQuery_Filter,
EventsQuery_IDFilter,
EventsQuery_LogLevelFilter,
EventsQuery_ModuleFilter,
EventsQuery_RequestFilter,
EventsQuery_TimeFilter,
type LogLevel,
Expand Down Expand Up @@ -56,7 +57,7 @@ export const modulesFilter = (modules: string[]): EventsQuery_Filter => {
return filter
}

export const callFilter = (destModule: string, destVerb: string | undefined = undefined, sourceModule: string | undefined = undefined): EventsQuery_Filter => {
export const callFilter = (destModule: string, destVerb?: string, sourceModule?: string): EventsQuery_Filter => {
const filter = new EventsQuery_Filter()
const callFilter = new EventsQuery_CallFilter()
callFilter.destModule = destModule
Expand All @@ -69,6 +70,18 @@ export const callFilter = (destModule: string, destVerb: string | undefined = un
return filter
}

export const moduleFilter = (module: string, verb?: string): EventsQuery_Filter => {
const filter = new EventsQuery_Filter()
const moduleFilter = new EventsQuery_ModuleFilter()
moduleFilter.module = module
moduleFilter.verb = verb
filter.filter = {
case: 'module',
value: moduleFilter,
}
return filter
}

export const timeFilter = (olderThan: Timestamp | undefined, newerThan: Timestamp | undefined): EventsQuery_Filter => {
const filter = new EventsQuery_Filter()
const timeFilter = new EventsQuery_TimeFilter()
Expand Down
15 changes: 15 additions & 0 deletions frontend/console/src/api/timeline/use-module-trace-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { EventType, type EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts'
import { eventTypesFilter, moduleFilter } from './timeline-filters.ts'
import { useTimeline } from './use-timeline.ts'

export const useModuleTraceEvents = (module: string, verb?: string, filters: EventsQuery_Filter[] = []) => {
const eventTypes = [EventType.CALL, EventType.INGRESS]
const allFilters = [...filters, moduleFilter(module, verb), eventTypesFilter(eventTypes)]
const timelineQuery = useTimeline(true, allFilters, 500)

const data = timelineQuery.data?.filter((event) => event.entry.case === 'call' || event.entry.case === 'ingress') ?? []
return {
...timelineQuery,
data,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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 timelineQuery = useTimeline(true, allFilters, 500, !!requestKey)

const data = timelineQuery.data?.filter((event) => event.entry.case === 'call' || event.entry.case === 'ingress') ?? []
return {
Expand Down
2 changes: 1 addition & 1 deletion frontend/console/src/api/timeline/use-timeline-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTimeline } from './use-timeline.ts'

export const useTimelineCalls = (isStreaming: boolean, filters: EventsQuery_Filter[], enabled = true) => {
const allFilters = [...filters, eventTypesFilter([EventType.CALL])]
const timelineQuery = useTimeline(isStreaming, allFilters, enabled)
const timelineQuery = useTimeline(isStreaming, allFilters, 1000, enabled)

const data = timelineQuery.data || []
return {
Expand Down
18 changes: 11 additions & 7 deletions frontend/console/src/api/timeline/use-timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useClient } from '../../hooks/use-client'
import { useVisibility } from '../../hooks/use-visibility'
import { ConsoleService } from '../../protos/xyz/block/ftl/v1/console/console_connect'
import { type EventsQuery_Filter, EventsQuery_Order } from '../../protos/xyz/block/ftl/v1/console/console_pb'
import { type Event, type EventsQuery_Filter, EventsQuery_Order } from '../../protos/xyz/block/ftl/v1/console/console_pb'

const timelineKey = 'timeline'
const maxTimelineEntries = 1000

export const useTimeline = (isStreaming: boolean, filters: EventsQuery_Filter[], enabled = true) => {
export const useTimeline = (isStreaming: boolean, filters: EventsQuery_Filter[], updateIntervalMs = 1000, enabled = true) => {
const client = useClient(ConsoleService)
const queryClient = useQueryClient()
const isVisible = useVisibility()
Expand Down Expand Up @@ -36,12 +36,16 @@ export const useTimeline = (isStreaming: boolean, filters: EventsQuery_Filter[],
const streamTimeline = async ({ signal }: { signal: AbortSignal }) => {
try {
console.debug('streaming timeline')
console.debug('filters:', filters)
for await (const response of client.streamEvents({ updateInterval: { seconds: BigInt(1) }, query: { limit, filters, order } }, { signal })) {
console.debug('timeline-filters:', filters)
for await (const response of client.streamEvents(
{ updateInterval: { seconds: BigInt(0), nanos: updateIntervalMs * 1000 }, query: { limit, filters, order } },
{ signal },
)) {
console.debug('timeline-response:', response)
if (response.events) {
const prev = queryClient.getQueryData<Event[]>(queryKey) ?? []
const allEvents = [...response.events, ...prev].slice(0, maxTimelineEntries)
queryClient.setQueryData(queryKey, allEvents)
queryClient.setQueryData<Event[]>(queryKey, (prev = []) => {
return [...response.events, ...prev].slice(0, maxTimelineEntries)
})
}
}
} catch (error) {
Expand Down
82 changes: 0 additions & 82 deletions frontend/console/src/features/calls/CallList.tsx

This file was deleted.

75 changes: 2 additions & 73 deletions frontend/console/src/features/timeline/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,7 @@ import { timeFilter, useTimeline } from '../../api/timeline/index.ts'
import { Loader } from '../../components/Loader.tsx'
import type { Event, EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts'
import { SidePanelContext } from '../../providers/side-panel-provider.tsx'
import { formatTimestampShort } from '../../utils/date.utils.ts'
import { panelColor } from '../../utils/style.utils.ts'
import { deploymentTextColor } from '../deployments/deployment.utils.ts'
import { TimelineCall } from './TimelineCall.tsx'
import { TimelineDeploymentCreated } from './TimelineDeploymentCreated.tsx'
import { TimelineDeploymentUpdated } from './TimelineDeploymentUpdated.tsx'
import { TimelineIcon } from './TimelineIcon.tsx'
import { TimelineIngress } from './TimelineIngress.tsx'
import { TimelineLog } from './TimelineLog.tsx'
import TimelineEventList from './TimelineEventList.tsx'
import { TimelineCallDetails } from './details/TimelineCallDetails.tsx'
import { TimelineDeploymentCreatedDetails } from './details/TimelineDeploymentCreatedDetails.tsx'
import { TimelineDeploymentUpdatedDetails } from './details/TimelineDeploymentUpdatedDetails.tsx'
Expand Down Expand Up @@ -76,23 +68,6 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings
setSearchParams({ ...Object.fromEntries(searchParams.entries()), id: entry.id.toString() })
}

const deploymentKey = (event: Event) => {
switch (event.entry?.case) {
case 'call':
return event.entry.value.deploymentKey
case 'log':
return event.entry.value.deploymentKey
case 'deploymentCreated':
return event.entry.value.key
case 'deploymentUpdated':
return event.entry.value.key
case 'ingress':
return event.entry.value.deploymentKey
default:
return ''
}
}

if (timeline.isLoading) {
return (
<div className='flex justify-center items-center min-h-screen'>
Expand All @@ -105,53 +80,7 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings

return (
<div className='border border-gray-100 dark:border-slate-700 rounded m-2'>
<div className='overflow-x-hidden'>
<table className={'w-full table-fixed text-gray-600 dark:text-gray-300'}>
<thead>
<tr className='flex text-xs'>
<th className='p-1 text-left border-b w-8 border-gray-100 dark:border-slate-700 flex-none' />
<th className='p-1 text-left border-b w-40 border-gray-100 dark:border-slate-700 flex-none'>Date</th>
<th className='p-1 text-left border-b w-40 border-gray-100 dark:border-slate-700 flex-none'>Deployment</th>
<th className='p-1 text-left border-b border-gray-100 dark:border-slate-700 flex-grow flex-shrink'>Content</th>
</tr>
</thead>
<tbody>
{entries.map((entry) => (
<tr
key={entry.id.toString()}
className={`flex border-b border-gray-100 dark:border-slate-700 text-xs font-roboto-mono ${
selectedEntry?.id === entry.id ? 'bg-indigo-50 dark:bg-slate-700' : panelColor
} relative flex cursor-pointer hover:bg-indigo-50 dark:hover:bg-slate-700`}
onClick={() => handleEntryClicked(entry)}
>
<td className='w-8 flex-none flex items-center justify-center'>
<TimelineIcon event={entry} />
</td>
<td className='p-1 w-40 items-center flex-none text-gray-400 dark:text-gray-400'>{formatTimestampShort(entry.timeStamp)}</td>
<td title={deploymentKey(entry)} className={`p-1 pr-2 w-40 items-center flex-none truncate ${deploymentTextColor(deploymentKey(entry))}`}>
{deploymentKey(entry)}
</td>
<td className='p-1 flex-grow truncate'>
{(() => {
switch (entry.entry?.case) {
case 'call':
return <TimelineCall call={entry.entry.value} />
case 'log':
return <TimelineLog log={entry.entry.value} />
case 'deploymentCreated':
return <TimelineDeploymentCreated deployment={entry.entry.value} />
case 'deploymentUpdated':
return <TimelineDeploymentUpdated deployment={entry.entry.value} />
case 'ingress':
return <TimelineIngress ingress={entry.entry.value} />
}
})()}
</td>
</tr>
))}
</tbody>
</table>
</div>
<TimelineEventList events={entries} selectedEventId={selectedEntry?.id} handleEntryClicked={handleEntryClicked} />
</div>
)
}
Loading

0 comments on commit 6aa5993

Please sign in to comment.