(cli): update queue details view

This commit is contained in:
Ken Hibino
2022-05-18 11:13:24 -07:00
parent 2c43ee9e20
commit 16856a6a41
5 changed files with 124 additions and 29 deletions

View File

@@ -19,7 +19,7 @@ func init() {
rootCmd.AddCommand(dashCmd) rootCmd.AddCommand(dashCmd)
// TODO: Remove this debug once we're done // TODO: Remove this debug once we're done
dashCmd.Flags().BoolVar(&flagDebug, "debug", false, "Print debug info") dashCmd.Flags().BoolVar(&flagDebug, "debug", false, "Print debug info")
dashCmd.Flags().BoolVar(&flagUseRealData, "realdata", false, "Use real data in redis") dashCmd.Flags().BoolVar(&flagUseRealData, "realdata", true, "Use real data in redis")
} }
var dashCmd = &cobra.Command{ var dashCmd = &cobra.Command{

View File

@@ -28,10 +28,12 @@ const (
// State holds dashboard state. // State holds dashboard state.
type State struct { type State struct {
queues []*asynq.QueueInfo queues []*asynq.QueueInfo
tasks []*asynq.TaskInfo
redisInfo redisInfo redisInfo redisInfo
err error err error
rowIdx int // highlighted row queueTableRowIdx int // highlighted row in queue table
taskTableRowIdx int // highlighted row in task table
taskState asynq.TaskState // highlighted task state in queue details view taskState asynq.TaskState // highlighted task state in queue details view
selectedQueue *asynq.QueueInfo // queue shown on queue details view selectedQueue *asynq.QueueInfo // queue shown on queue details view
@@ -73,6 +75,7 @@ func Run(opts Options) {
var ( var (
errorCh = make(chan error) errorCh = make(chan error)
queuesCh = make(chan []*asynq.QueueInfo) queuesCh = make(chan []*asynq.QueueInfo)
tasksCh = make(chan []*asynq.TaskInfo)
redisInfoCh = make(chan *redisInfo) redisInfoCh = make(chan *redisInfo)
) )
@@ -123,25 +126,41 @@ func Run(opts Options) {
quit() quit()
} else if ev.Key() == tcell.KeyCtrlL { } else if ev.Key() == tcell.KeyCtrlL {
s.Sync() s.Sync()
} else if ev.Key() == tcell.KeyDown || ev.Rune() == 'j' { } else if (ev.Key() == tcell.KeyDown || ev.Rune() == 'j') && state.view == viewTypeQueues {
if state.rowIdx < len(state.queues) { if state.queueTableRowIdx < len(state.queues) {
state.rowIdx++ state.queueTableRowIdx++
} else { } else {
state.rowIdx = 0 // loop back state.queueTableRowIdx = 0 // loop back
} }
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if ev.Key() == tcell.KeyUp || ev.Rune() == 'k' { } else if (ev.Key() == tcell.KeyUp || ev.Rune() == 'k') && state.view == viewTypeQueues {
if state.rowIdx == 0 { if state.queueTableRowIdx == 0 {
state.rowIdx = len(state.queues) state.queueTableRowIdx = len(state.queues)
} else { } else {
state.rowIdx-- state.queueTableRowIdx--
}
drawDash(s, baseStyle, &state, opts)
} else if (ev.Key() == tcell.KeyDown || ev.Rune() == 'j') && state.view == viewTypeQueueDetails {
if state.taskTableRowIdx < len(state.tasks) {
state.taskTableRowIdx++
} else {
state.taskTableRowIdx = 0 // loop back
}
drawDash(s, baseStyle, &state, opts)
} else if (ev.Key() == tcell.KeyUp || ev.Rune() == 'k') && state.view == viewTypeQueueDetails {
if state.taskTableRowIdx == 0 {
state.taskTableRowIdx = len(state.tasks)
} else {
state.taskTableRowIdx--
} }
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if ev.Key() == tcell.KeyEnter { } else if ev.Key() == tcell.KeyEnter {
if state.view == viewTypeQueues && state.rowIdx != 0 { if state.view == viewTypeQueues && state.queueTableRowIdx != 0 {
state.selectedQueue = state.queues[state.rowIdx-1] state.selectedQueue = state.queues[state.queueTableRowIdx-1]
state.view = viewTypeQueueDetails state.view = viewTypeQueueDetails
state.taskState = asynq.TaskStateActive state.taskState = asynq.TaskStateActive
state.tasks = nil
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, tasksCh, errorCh)
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} }
} else if ev.Rune() == '?' { } else if ev.Rune() == '?' {
@@ -168,9 +187,13 @@ func Run(opts Options) {
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if (ev.Key() == tcell.KeyRight || ev.Rune() == 'l') && state.view == viewTypeQueueDetails { } else if (ev.Key() == tcell.KeyRight || ev.Rune() == 'l') && state.view == viewTypeQueueDetails {
state.taskState = nextTaskState(state.taskState) state.taskState = nextTaskState(state.taskState)
state.tasks = nil
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, tasksCh, errorCh)
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} else if (ev.Key() == tcell.KeyLeft || ev.Rune() == 'h') && state.view == viewTypeQueueDetails { } else if (ev.Key() == tcell.KeyLeft || ev.Rune() == 'h') && state.view == viewTypeQueueDetails {
state.taskState = prevTaskState(state.taskState) state.taskState = prevTaskState(state.taskState)
state.tasks = nil
go fetchTasks(inspector, state.selectedQueue.Queue, state.taskState, tasksCh, errorCh)
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
} }
} }
@@ -188,6 +211,11 @@ func Run(opts Options) {
state.err = nil state.err = nil
drawDash(s, baseStyle, &state, opts) drawDash(s, baseStyle, &state, opts)
case tasks := <-tasksCh:
state.tasks = tasks
state.err = nil
drawDash(s, baseStyle, &state, opts)
case redisInfo := <-redisInfoCh: case redisInfo := <-redisInfoCh:
state.redisInfo = *redisInfo state.redisInfo = *redisInfo
state.err = nil state.err = nil

View File

@@ -9,6 +9,7 @@ import (
"math" "math"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
@@ -27,12 +28,16 @@ func drawDash(s tcell.Screen, style tcell.Style, state *State, opts Options) {
d.NL() d.NL()
drawQueueTable(d, style, state) drawQueueTable(d, style, state)
case viewTypeQueueDetails: case viewTypeQueueDetails:
d.Println(fmt.Sprintf("=== Queues > %s ===", state.selectedQueue.Queue), style.Bold(true)) d.Println("=== Queue Summary ===", style.Bold(true))
d.NL() d.NL()
drawQueueInfoBanner(d, style, state) drawQueueSummary(d, style, state)
d.NL()
d.NL()
d.Println("=== Tasks ===", style.Bold(true))
d.NL() d.NL()
d.Println("+++ Tasks +++", style.Bold(true))
drawTaskStateBreakdown(d, style, state) drawTaskStateBreakdown(d, style, state)
d.NL()
drawTaskTable(d, style, state)
case viewTypeServers: case viewTypeServers:
d.Println("=== Servers ===", style.Bold(true)) d.Println("=== Servers ===", style.Bold(true))
d.NL() d.NL()
@@ -54,7 +59,7 @@ func drawDash(s tcell.Screen, style tcell.Style, state *State, opts Options) {
// TODO: Draw HELP body // TODO: Draw HELP body
} }
if opts.DebugMode { if opts.DebugMode {
d.Println(fmt.Sprintf("DEBUG: rowIdx = %d", state.rowIdx), style) d.Println(fmt.Sprintf("DEBUG: rowIdx = %d", state.queueTableRowIdx), style)
d.Println(fmt.Sprintf("DEBUG: selectedQueue = %s", state.selectedQueue.Queue), style) d.Println(fmt.Sprintf("DEBUG: selectedQueue = %s", state.selectedQueue.Queue), style)
d.Println(fmt.Sprintf("DEBUG: view = %v", state.view), style) d.Println(fmt.Sprintf("DEBUG: view = %v", state.view), style)
} }
@@ -215,19 +220,53 @@ var queueColumnConfigs = []*columnConfig[*asynq.QueueInfo]{
} }
}}, }},
{"Size", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Size) }}, {"Size", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Size) }},
{"Latency", alignRight, func(q *asynq.QueueInfo) string { return q.Latency.String() }}, {"Latency", alignRight, func(q *asynq.QueueInfo) string { return q.Latency.Round(time.Second).String() }},
{"MemoryUsage", alignRight, func(q *asynq.QueueInfo) string { return ByteCount(q.MemoryUsage) }}, {"MemoryUsage", alignRight, func(q *asynq.QueueInfo) string { return ByteCount(q.MemoryUsage) }},
{"Processed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Processed) }}, {"Processed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Processed) }},
{"Failed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Failed) }}, {"Failed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Failed) }},
{"ErrorRate", alignRight, func(q *asynq.QueueInfo) string { return "0.23%" /* TODO: implement this */ }}, {"ErrorRate", alignRight, func(q *asynq.QueueInfo) string { return formatErrorRate(q.Processed, q.Failed) }},
}
func formatErrorRate(processed, failed int) string {
if processed == 0 {
return "-"
}
return fmt.Sprintf("%.2f", float64(failed)/float64(processed))
} }
func drawQueueTable(d *ScreenDrawer, style tcell.Style, state *State) { func drawQueueTable(d *ScreenDrawer, style tcell.Style, state *State) {
drawTable(d, style, queueColumnConfigs, state.queues, state.rowIdx-1) drawTable(d, style, queueColumnConfigs, state.queues, state.queueTableRowIdx-1)
} }
func drawQueueInfoBanner(d *ScreenDrawer, style tcell.Style, state *State) { func drawQueueSummary(d *ScreenDrawer, style tcell.Style, state *State) {
drawTable(d, style, queueColumnConfigs, []*asynq.QueueInfo{state.selectedQueue}, -1 /* no highlited row */) q := state.selectedQueue
labelStyle := style.Foreground(tcell.ColorLightGray)
d.Print("Name: ", labelStyle)
d.Println(q.Queue, style)
d.Print("Size: ", labelStyle)
d.Println(strconv.Itoa(q.Size), style)
d.Print("Latency ", labelStyle)
d.Println(q.Latency.Round(time.Second).String(), style)
d.Print("MemUsage ", labelStyle)
d.Println(ByteCount(q.MemoryUsage), style)
}
func drawTaskTable(d *ScreenDrawer, style tcell.Style, state *State) {
if state.taskState == asynq.TaskStateAggregating {
d.Println("TODO: aggregating tasks need group name", style)
return
}
if len(state.tasks) == 0 {
return // print nothing
}
colConfigs := []*columnConfig[*asynq.TaskInfo]{
{"ID", alignLeft, func(t *asynq.TaskInfo) string { return t.ID }},
{"Type", alignLeft, func(t *asynq.TaskInfo) string { return t.Type }},
{"Payload", alignLeft, func(t *asynq.TaskInfo) string { return string(t.Payload) }},
{"MaxRetry", alignRight, func(t *asynq.TaskInfo) string { return strconv.Itoa(t.MaxRetry) }},
{"LastError", alignLeft, func(t *asynq.TaskInfo) string { return t.LastErr }},
}
drawTable(d, style, colConfigs, state.tasks, state.taskTableRowIdx-1)
} }
// Define the order of states to show // Define the order of states to show
@@ -292,9 +331,10 @@ func drawTaskStateBreakdown(d *ScreenDrawer, style tcell.Style, state *State) {
for _, ts := range taskStates { for _, ts := range taskStates {
s := style s := style
if state.taskState == ts { if state.taskState == ts {
s = s.Background(tcell.ColorDarkOliveGreen) s = s.Bold(true).Underline(true)
} }
d.Print(fmt.Sprintf("%s:%d", ts.String(), getTaskCount(state.selectedQueue, ts)), s) d.Print(fmt.Sprintf("%s:%d", strings.Title(ts.String()), getTaskCount(state.selectedQueue, ts)), s)
d.Print(pad, style) d.Print(pad, style)
} }
d.NL()
} }

View File

@@ -47,3 +47,29 @@ func fetchRedisInfo(redisInfoCh chan<- *redisInfo, errorCh chan<- error) {
peakMemoryUsage: n + 123, peakMemoryUsage: n + 123,
} }
} }
func fetchTasks(i *asynq.Inspector, qname string, taskState asynq.TaskState, tasksCh chan<- []*asynq.TaskInfo, errorCh chan<- error) {
var (
tasks []*asynq.TaskInfo
err error
)
switch taskState {
case asynq.TaskStateActive:
tasks, err = i.ListActiveTasks(qname)
case asynq.TaskStatePending:
tasks, err = i.ListPendingTasks(qname)
case asynq.TaskStateScheduled:
tasks, err = i.ListScheduledTasks(qname)
case asynq.TaskStateRetry:
tasks, err = i.ListRetryTasks(qname)
case asynq.TaskStateArchived:
tasks, err = i.ListArchivedTasks(qname)
case asynq.TaskStateCompleted:
tasks, err = i.ListCompletedTasks(qname)
}
if err != nil {
errorCh <- err
return
}
tasksCh <- tasks
}

View File

@@ -27,9 +27,10 @@ type column[V any] struct {
width int width int
} }
// Helper to draw a table. // Helper to draw a table.
func drawTable[V any](d *ScreenDrawer, style tcell.Style, configs []*columnConfig[V], data []V, highlightRowIdx int) { func drawTable[V any](d *ScreenDrawer, style tcell.Style, configs []*columnConfig[V], data []V, highlightRowIdx int) {
const colBuffer = 4 // extra buffer between columns const colBuffer = " " // extra buffer between columns
cols := make([]*column[V], len(configs)) cols := make([]*column[V], len(configs))
for i, cfg := range configs { for i, cfg := range configs {
cols[i] = &column[V]{cfg, runewidth.StringWidth(cfg.name)} cols[i] = &column[V]{cfg, runewidth.StringWidth(cfg.name)}
@@ -46,9 +47,9 @@ func drawTable[V any](d *ScreenDrawer, style tcell.Style, configs []*columnConfi
headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite) headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite)
for _, col := range cols { for _, col := range cols {
if col.alignment == alignLeft { if col.alignment == alignLeft {
d.Print(rpad(col.name, col.width+colBuffer), headerStyle) d.Print(rpad(col.name, col.width) + colBuffer, headerStyle)
} else { } else {
d.Print(lpad(col.name, col.width+colBuffer), headerStyle) d.Print(lpad(col.name, col.width) + colBuffer, headerStyle)
} }
} }
d.FillLine(' ', headerStyle) d.FillLine(' ', headerStyle)
@@ -60,9 +61,9 @@ func drawTable[V any](d *ScreenDrawer, style tcell.Style, configs []*columnConfi
} }
for _, col := range cols { for _, col := range cols {
if col.alignment == alignLeft { if col.alignment == alignLeft {
d.Print(rpad(col.displayFn(v), col.width+colBuffer), rowStyle) d.Print(rpad(col.displayFn(v), col.width) + colBuffer, rowStyle)
} else { } else {
d.Print(lpad(col.displayFn(v), col.width+colBuffer), rowStyle) d.Print(lpad(col.displayFn(v), col.width) + colBuffer, rowStyle)
} }
} }
d.FillLine(' ', rowStyle) d.FillLine(' ', rowStyle)