mirror of
https://github.com/hibiken/asynq.git
synced 2025-09-19 13:21:58 +08:00
(cli): Split dash package into multiple files
This commit is contained in:
@@ -6,16 +6,11 @@ package dash
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// viewType is an enum for dashboard views.
|
// viewType is an enum for dashboard views.
|
||||||
@@ -30,7 +25,8 @@ const (
|
|||||||
viewTypeHelp
|
viewTypeHelp
|
||||||
)
|
)
|
||||||
|
|
||||||
type dashState struct {
|
// State holds dashboard state.
|
||||||
|
type State struct {
|
||||||
queues []*asynq.QueueInfo
|
queues []*asynq.QueueInfo
|
||||||
redisInfo redisInfo
|
redisInfo redisInfo
|
||||||
err error
|
err error
|
||||||
@@ -78,9 +74,9 @@ func Run(opts Options) {
|
|||||||
redisInfoCh = make(chan *redisInfo)
|
redisInfoCh = make(chan *redisInfo)
|
||||||
)
|
)
|
||||||
|
|
||||||
go getQueueInfo(inspector, queuesCh, errorCh, opts)
|
go fetchQueueInfo(inspector, queuesCh, errorCh, opts)
|
||||||
|
|
||||||
var state dashState // contained in this goroutine only; do not share
|
var state State // contained in this goroutine only; do not share
|
||||||
|
|
||||||
// draw initial screen
|
// draw initial screen
|
||||||
drawDash(s, baseStyle, &state, opts)
|
drawDash(s, baseStyle, &state, opts)
|
||||||
@@ -150,7 +146,7 @@ func Run(opts Options) {
|
|||||||
state.view = viewTypeHelp
|
state.view = viewTypeHelp
|
||||||
drawDash(s, baseStyle, &state, opts)
|
drawDash(s, baseStyle, &state, opts)
|
||||||
} else if ev.Key() == tcell.KeyF1 && state.view != viewTypeQueues {
|
} else if ev.Key() == tcell.KeyF1 && state.view != viewTypeQueues {
|
||||||
go getQueueInfo(inspector, queuesCh, errorCh, opts)
|
go fetchQueueInfo(inspector, queuesCh, errorCh, opts)
|
||||||
ticker.Reset(interval)
|
ticker.Reset(interval)
|
||||||
state.view = viewTypeQueues
|
state.view = viewTypeQueues
|
||||||
drawDash(s, baseStyle, &state, opts)
|
drawDash(s, baseStyle, &state, opts)
|
||||||
@@ -163,7 +159,7 @@ func Run(opts Options) {
|
|||||||
state.view = viewTypeSchedulers
|
state.view = viewTypeSchedulers
|
||||||
drawDash(s, baseStyle, &state, opts)
|
drawDash(s, baseStyle, &state, opts)
|
||||||
} else if ev.Key() == tcell.KeyF4 && state.view != viewTypeRedis {
|
} else if ev.Key() == tcell.KeyF4 && state.view != viewTypeRedis {
|
||||||
go getRedisInfo(redisInfoCh, errorCh)
|
go fetchRedisInfo(redisInfoCh, errorCh)
|
||||||
ticker.Reset(interval)
|
ticker.Reset(interval)
|
||||||
state.view = viewTypeRedis
|
state.view = viewTypeRedis
|
||||||
drawDash(s, baseStyle, &state, opts)
|
drawDash(s, baseStyle, &state, opts)
|
||||||
@@ -173,9 +169,9 @@ func Run(opts Options) {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
switch state.view {
|
switch state.view {
|
||||||
case viewTypeQueues:
|
case viewTypeQueues:
|
||||||
go getQueueInfo(inspector, queuesCh, errorCh, opts)
|
go fetchQueueInfo(inspector, queuesCh, errorCh, opts)
|
||||||
case viewTypeRedis:
|
case viewTypeRedis:
|
||||||
go getRedisInfo(redisInfoCh, errorCh)
|
go fetchRedisInfo(redisInfoCh, errorCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
case queues := <-queuesCh:
|
case queues := <-queuesCh:
|
||||||
@@ -195,378 +191,3 @@ func Run(opts Options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQueueInfo(i *asynq.Inspector, queuesCh chan<- []*asynq.QueueInfo, errorCh chan<- error, opts Options) {
|
|
||||||
if !opts.UseRealData {
|
|
||||||
n := rand.Intn(100)
|
|
||||||
queuesCh <- []*asynq.QueueInfo{
|
|
||||||
{Queue: "default", Size: 1800 + n, Pending: 700 + n, Active: 300, Aggregating: 300, Scheduled: 200, Retry: 100, Archived: 200},
|
|
||||||
{Queue: "critical", Size: 2300 + n, Pending: 1000 + n, Active: 500, Retry: 400, Completed: 400},
|
|
||||||
{Queue: "low", Size: 900 + n, Pending: n, Active: 300, Scheduled: 400, Completed: 200},
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
queues, err := i.Queues()
|
|
||||||
if err != nil {
|
|
||||||
errorCh <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var res []*asynq.QueueInfo
|
|
||||||
for _, q := range queues {
|
|
||||||
info, err := i.GetQueueInfo(q)
|
|
||||||
if err != nil {
|
|
||||||
errorCh <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res = append(res, info)
|
|
||||||
}
|
|
||||||
queuesCh <- res
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRedisInfo(redisInfoCh chan<- *redisInfo, errorCh chan<- error) {
|
|
||||||
n := rand.Intn(1000)
|
|
||||||
redisInfoCh <- &redisInfo{
|
|
||||||
version: "6.2.6",
|
|
||||||
uptime: "9 days",
|
|
||||||
memoryUsage: n,
|
|
||||||
peakMemoryUsage: n + 123,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawDash(s tcell.Screen, style tcell.Style, state *dashState, opts Options) {
|
|
||||||
s.Clear()
|
|
||||||
// Simulate data update on every render
|
|
||||||
d := NewScreenDrawer(s)
|
|
||||||
switch state.view {
|
|
||||||
case viewTypeQueues:
|
|
||||||
d.Println("=== Queues ===", style.Bold(true))
|
|
||||||
d.NL() // empty line
|
|
||||||
drawQueueSizeGraphs(d, style, state)
|
|
||||||
d.NL() // empty line
|
|
||||||
drawQueueTable(d, style, state)
|
|
||||||
case viewTypeQueueDetails:
|
|
||||||
d.Println(fmt.Sprintf("=== Queues > %s ===", state.selectedQueue), style)
|
|
||||||
d.NL()
|
|
||||||
// TODO: draw body
|
|
||||||
case viewTypeServers:
|
|
||||||
d.Println("=== Servers ===", style.Bold(true))
|
|
||||||
d.NL() // empty line
|
|
||||||
// TODO: Draw body
|
|
||||||
case viewTypeSchedulers:
|
|
||||||
d.Println("=== Schedulers === ", style.Bold(true))
|
|
||||||
d.NL() // empty line
|
|
||||||
// TODO: Draw body
|
|
||||||
case viewTypeRedis:
|
|
||||||
d.Println("=== Redis Info === ", style.Bold(true))
|
|
||||||
d.NL() // empty line
|
|
||||||
d.Println(fmt.Sprintf("Version: %s", state.redisInfo.version), style)
|
|
||||||
d.Println(fmt.Sprintf("Uptime: %s", state.redisInfo.uptime), style)
|
|
||||||
d.Println(fmt.Sprintf("Memory Usage: %s", ByteCount(int64(state.redisInfo.memoryUsage))), style)
|
|
||||||
d.Println(fmt.Sprintf("Peak Memory Usage: %s", ByteCount(int64(state.redisInfo.peakMemoryUsage))), style)
|
|
||||||
case viewTypeHelp:
|
|
||||||
d.Println("=== HELP ===", style.Bold(true))
|
|
||||||
d.NL() // empty line
|
|
||||||
// TODO: Draw HELP body
|
|
||||||
}
|
|
||||||
if opts.DebugMode {
|
|
||||||
d.Println(fmt.Sprintf("DEBUG: rowIdx = %d", state.rowIdx), style)
|
|
||||||
d.Println(fmt.Sprintf("DEBUG: selectedQueue = %s", state.selectedQueue), style)
|
|
||||||
d.Println(fmt.Sprintf("DEBUG: view = %v", state.view), style)
|
|
||||||
}
|
|
||||||
d.GoToBottom()
|
|
||||||
drawFooter(d, style, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawQueueSizeGraphs(d *ScreenDrawer, style tcell.Style, state *dashState) {
|
|
||||||
var (
|
|
||||||
activeStyle = tcell.StyleDefault.Foreground(tcell.GetColor("blue")).Background(tcell.ColorReset)
|
|
||||||
pendingStyle = tcell.StyleDefault.Foreground(tcell.GetColor("green")).Background(tcell.ColorReset)
|
|
||||||
aggregatingStyle = tcell.StyleDefault.Foreground(tcell.GetColor("lightgreen")).Background(tcell.ColorReset)
|
|
||||||
scheduledStyle = tcell.StyleDefault.Foreground(tcell.GetColor("yellow")).Background(tcell.ColorReset)
|
|
||||||
retryStyle = tcell.StyleDefault.Foreground(tcell.GetColor("pink")).Background(tcell.ColorReset)
|
|
||||||
archivedStyle = tcell.StyleDefault.Foreground(tcell.GetColor("purple")).Background(tcell.ColorReset)
|
|
||||||
completedStyle = tcell.StyleDefault.Foreground(tcell.GetColor("darkgreen")).Background(tcell.ColorReset)
|
|
||||||
)
|
|
||||||
|
|
||||||
var qnames []string
|
|
||||||
var qsizes []string // queue size in strings
|
|
||||||
maxSize := 1 // not zero to avoid division by zero
|
|
||||||
for _, q := range state.queues {
|
|
||||||
qnames = append(qnames, q.Queue)
|
|
||||||
qsizes = append(qsizes, strconv.Itoa(q.Size))
|
|
||||||
if q.Size > maxSize {
|
|
||||||
maxSize = q.Size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
qnameWidth := maxwidth(qnames)
|
|
||||||
qsizeWidth := maxwidth(qsizes)
|
|
||||||
|
|
||||||
// Calculate the multipler to scale the graph
|
|
||||||
screenWidth, _ := d.Screen().Size()
|
|
||||||
graphMaxWidth := screenWidth - (qnameWidth + qsizeWidth + 3) // <qname> |<graph> <size>
|
|
||||||
multipiler := 1.0
|
|
||||||
if graphMaxWidth < maxSize {
|
|
||||||
multipiler = float64(graphMaxWidth) / float64(maxSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tick = '▇'
|
|
||||||
for _, q := range state.queues {
|
|
||||||
d.Print(q.Queue, style)
|
|
||||||
d.Print(strings.Repeat(" ", qnameWidth-runewidth.StringWidth(q.Queue)+1), style) // padding between qname and graph
|
|
||||||
d.Print("|", style)
|
|
||||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Completed)*multipiler))), completedStyle)
|
|
||||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Archived)*multipiler))), archivedStyle)
|
|
||||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Retry)*multipiler))), retryStyle)
|
|
||||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Scheduled)*multipiler))), scheduledStyle)
|
|
||||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Aggregating)*multipiler))), aggregatingStyle)
|
|
||||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Pending)*multipiler))), pendingStyle)
|
|
||||||
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Active)*multipiler))), activeStyle)
|
|
||||||
d.Print(fmt.Sprintf(" %d", q.Size), style)
|
|
||||||
d.NL()
|
|
||||||
}
|
|
||||||
d.NL()
|
|
||||||
d.Print("completed=", style)
|
|
||||||
d.Print(string(tick), completedStyle)
|
|
||||||
d.Print(" archived=", style)
|
|
||||||
d.Print(string(tick), archivedStyle)
|
|
||||||
d.Print(" retry=", style)
|
|
||||||
d.Print(string(tick), retryStyle)
|
|
||||||
d.Print(" scheduled=", style)
|
|
||||||
d.Print(string(tick), scheduledStyle)
|
|
||||||
d.Print(" aggregating=", style)
|
|
||||||
d.Print(string(tick), aggregatingStyle)
|
|
||||||
d.Print(" pending=", style)
|
|
||||||
d.Print(string(tick), pendingStyle)
|
|
||||||
d.Print(" active=", style)
|
|
||||||
d.Print(string(tick), activeStyle)
|
|
||||||
d.NL()
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawFooter(d *ScreenDrawer, baseStyle tcell.Style, state *dashState) {
|
|
||||||
if state.err != nil {
|
|
||||||
style := baseStyle.Background(tcell.ColorDarkRed)
|
|
||||||
d.Print(state.err.Error(), style)
|
|
||||||
d.FillLine(' ', style)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
style := baseStyle.Background(tcell.ColorDarkSlateGray)
|
|
||||||
switch state.view {
|
|
||||||
case viewTypeHelp:
|
|
||||||
d.Print("Esc=GoBack", style)
|
|
||||||
default:
|
|
||||||
type menu struct {
|
|
||||||
label string
|
|
||||||
view viewType
|
|
||||||
}
|
|
||||||
menus := []*menu{
|
|
||||||
{"F1=Queues", viewTypeQueues},
|
|
||||||
{"F2=Servers", viewTypeServers},
|
|
||||||
{"F3=Schedulers", viewTypeSchedulers},
|
|
||||||
{"F4=Redis", viewTypeRedis},
|
|
||||||
{"?=Help", viewTypeHelp},
|
|
||||||
}
|
|
||||||
var b strings.Builder
|
|
||||||
for _, m := range menus {
|
|
||||||
b.WriteString(m.label)
|
|
||||||
// Add * for the current view
|
|
||||||
if m.view == state.view {
|
|
||||||
b.WriteString("* ")
|
|
||||||
} else {
|
|
||||||
b.WriteString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.Print(b.String(), style)
|
|
||||||
}
|
|
||||||
d.FillLine(' ', style)
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns the maximum width from the given list of names
|
|
||||||
func maxwidth(names []string) int {
|
|
||||||
max := 0
|
|
||||||
for _, s := range names {
|
|
||||||
if w := runewidth.StringWidth(s); w > max {
|
|
||||||
max = w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByteCount converts the given bytes into human readable string
|
|
||||||
func ByteCount(b int64) string {
|
|
||||||
const unit = 1000
|
|
||||||
if b < unit {
|
|
||||||
return fmt.Sprintf("%d B", b)
|
|
||||||
|
|
||||||
}
|
|
||||||
div, exp := int64(unit), 0
|
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawQueueTable(d *ScreenDrawer, style tcell.Style, state *dashState) {
|
|
||||||
colConfigs := []*columnConfig[*asynq.QueueInfo]{
|
|
||||||
{"Queue", alignLeft, func(q *asynq.QueueInfo) string { return q.Queue }},
|
|
||||||
{"State", alignLeft, func(q *asynq.QueueInfo) string {
|
|
||||||
if q.Paused {
|
|
||||||
return "PAUSED"
|
|
||||||
} else {
|
|
||||||
return "RUN"
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
{"Size", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Size) }},
|
|
||||||
{"Latency", alignRight, func(q *asynq.QueueInfo) string { return q.Latency.String() }},
|
|
||||||
{"MemoryUsage", alignRight, func(q *asynq.QueueInfo) string { return ByteCount(q.MemoryUsage) }},
|
|
||||||
{"Processed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Processed) }},
|
|
||||||
{"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 */ }},
|
|
||||||
}
|
|
||||||
drawTable(d, style, colConfigs, state.queues, state.rowIdx-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
type columnAlignment int
|
|
||||||
|
|
||||||
const (
|
|
||||||
alignRight columnAlignment = iota
|
|
||||||
alignLeft
|
|
||||||
)
|
|
||||||
|
|
||||||
type columnConfig[V any] struct {
|
|
||||||
name string
|
|
||||||
alignment columnAlignment
|
|
||||||
displayFn func(v V) string
|
|
||||||
}
|
|
||||||
|
|
||||||
type column[V any] struct {
|
|
||||||
*columnConfig[V]
|
|
||||||
width int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to draw a table.
|
|
||||||
func drawTable[V any](d *ScreenDrawer, style tcell.Style, configs []*columnConfig[V], data []V, highlightRowIdx int) {
|
|
||||||
const colBuffer = 4 // extra buffer between columns
|
|
||||||
cols := make([]*column[V], len(configs))
|
|
||||||
for i, cfg := range configs {
|
|
||||||
cols[i] = &column[V]{cfg, runewidth.StringWidth(cfg.name)}
|
|
||||||
}
|
|
||||||
// adjust the column width to accommodate the widest value.
|
|
||||||
for _, v := range data {
|
|
||||||
for _, col := range cols {
|
|
||||||
if w := runewidth.StringWidth(col.displayFn(v)); col.width < w {
|
|
||||||
col.width = w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// print header
|
|
||||||
headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite)
|
|
||||||
for _, col := range cols {
|
|
||||||
if col.alignment == alignLeft {
|
|
||||||
d.Print(rpad(col.name, col.width+colBuffer), headerStyle)
|
|
||||||
} else {
|
|
||||||
d.Print(lpad(col.name, col.width+colBuffer), headerStyle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.FillLine(' ', headerStyle)
|
|
||||||
// print body
|
|
||||||
for i, v := range data {
|
|
||||||
rowStyle := style
|
|
||||||
if highlightRowIdx == i {
|
|
||||||
rowStyle = style.Background(tcell.ColorDarkOliveGreen)
|
|
||||||
}
|
|
||||||
for _, col := range cols {
|
|
||||||
if col.alignment == alignLeft {
|
|
||||||
d.Print(rpad(col.displayFn(v), col.width+colBuffer), rowStyle)
|
|
||||||
} else {
|
|
||||||
d.Print(lpad(col.displayFn(v), col.width+colBuffer), rowStyle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.FillLine(' ', rowStyle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Screen Drawer ***/
|
|
||||||
|
|
||||||
// ScreenDrawer is used to draw contents on screen.
|
|
||||||
//
|
|
||||||
// Usage example:
|
|
||||||
// d := NewScreenDrawer(s)
|
|
||||||
// d.Println("Hello world", mystyle)
|
|
||||||
// d.NL() // adds newline
|
|
||||||
// d.Print("foo", mystyle.Bold(true))
|
|
||||||
// d.Print("bar", mystyle.Italic(true))
|
|
||||||
type ScreenDrawer struct {
|
|
||||||
l *LineDrawer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewScreenDrawer(s tcell.Screen) *ScreenDrawer {
|
|
||||||
return &ScreenDrawer{l: NewLineDrawer(0, s)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ScreenDrawer) Print(s string, style tcell.Style) {
|
|
||||||
d.l.Draw(s, style)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ScreenDrawer) Println(s string, style tcell.Style) {
|
|
||||||
d.Print(s, style)
|
|
||||||
d.NL()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FillLine prints the given rune until the end of the current line
|
|
||||||
// and adds a newline.
|
|
||||||
func (d *ScreenDrawer) FillLine(r rune, style tcell.Style) {
|
|
||||||
w, _ := d.Screen().Size()
|
|
||||||
s := strings.Repeat(string(r), w-d.l.col)
|
|
||||||
d.Print(s, style)
|
|
||||||
d.NL()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NL adds a newline (i.e., moves to the next line).
|
|
||||||
func (d *ScreenDrawer) NL() {
|
|
||||||
d.l.row++
|
|
||||||
d.l.col = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ScreenDrawer) Screen() tcell.Screen {
|
|
||||||
return d.l.s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to the bottom of the screen.
|
|
||||||
func (d *ScreenDrawer) GoToBottom() {
|
|
||||||
_, h := d.Screen().Size()
|
|
||||||
d.l.row = h - 1
|
|
||||||
d.l.col = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type LineDrawer struct {
|
|
||||||
s tcell.Screen
|
|
||||||
row int
|
|
||||||
col int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLineDrawer(row int, s tcell.Screen) *LineDrawer {
|
|
||||||
return &LineDrawer{row: row, col: 0, s: s}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *LineDrawer) Draw(s string, style tcell.Style) {
|
|
||||||
for _, r := range s {
|
|
||||||
d.s.SetContent(d.col, d.row, r, nil, style)
|
|
||||||
d.col += runewidth.RuneWidth(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// rpad adds padding to the right of a string.
|
|
||||||
func rpad(s string, padding int) string {
|
|
||||||
tmpl := fmt.Sprintf("%%-%ds ", padding)
|
|
||||||
return fmt.Sprintf(tmpl, s)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// lpad adds padding to the left of a string.
|
|
||||||
func lpad(s string, padding int) string {
|
|
||||||
tmpl := fmt.Sprintf("%%%ds ", padding)
|
|
||||||
return fmt.Sprintf(tmpl, s)
|
|
||||||
}
|
|
||||||
|
283
tools/asynq/cmd/dash/draw.go
Normal file
283
tools/asynq/cmd/dash/draw.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package dash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func drawDash(s tcell.Screen, style tcell.Style, state *State, opts Options) {
|
||||||
|
s.Clear()
|
||||||
|
// Simulate data update on every render
|
||||||
|
d := NewScreenDrawer(s)
|
||||||
|
switch state.view {
|
||||||
|
case viewTypeQueues:
|
||||||
|
d.Println("=== Queues ===", style.Bold(true))
|
||||||
|
d.NL() // empty line
|
||||||
|
drawQueueSizeGraphs(d, style, state)
|
||||||
|
d.NL() // empty line
|
||||||
|
drawQueueTable(d, style, state)
|
||||||
|
case viewTypeQueueDetails:
|
||||||
|
d.Println(fmt.Sprintf("=== Queues > %s ===", state.selectedQueue), style)
|
||||||
|
d.NL()
|
||||||
|
// TODO: draw body
|
||||||
|
case viewTypeServers:
|
||||||
|
d.Println("=== Servers ===", style.Bold(true))
|
||||||
|
d.NL() // empty line
|
||||||
|
// TODO: Draw body
|
||||||
|
case viewTypeSchedulers:
|
||||||
|
d.Println("=== Schedulers === ", style.Bold(true))
|
||||||
|
d.NL() // empty line
|
||||||
|
// TODO: Draw body
|
||||||
|
case viewTypeRedis:
|
||||||
|
d.Println("=== Redis Info === ", style.Bold(true))
|
||||||
|
d.NL() // empty line
|
||||||
|
d.Println(fmt.Sprintf("Version: %s", state.redisInfo.version), style)
|
||||||
|
d.Println(fmt.Sprintf("Uptime: %s", state.redisInfo.uptime), style)
|
||||||
|
d.Println(fmt.Sprintf("Memory Usage: %s", ByteCount(int64(state.redisInfo.memoryUsage))), style)
|
||||||
|
d.Println(fmt.Sprintf("Peak Memory Usage: %s", ByteCount(int64(state.redisInfo.peakMemoryUsage))), style)
|
||||||
|
case viewTypeHelp:
|
||||||
|
d.Println("=== HELP ===", style.Bold(true))
|
||||||
|
d.NL() // empty line
|
||||||
|
// TODO: Draw HELP body
|
||||||
|
}
|
||||||
|
if opts.DebugMode {
|
||||||
|
d.Println(fmt.Sprintf("DEBUG: rowIdx = %d", state.rowIdx), style)
|
||||||
|
d.Println(fmt.Sprintf("DEBUG: selectedQueue = %s", state.selectedQueue), style)
|
||||||
|
d.Println(fmt.Sprintf("DEBUG: view = %v", state.view), style)
|
||||||
|
}
|
||||||
|
d.GoToBottom()
|
||||||
|
drawFooter(d, style, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawQueueSizeGraphs(d *ScreenDrawer, style tcell.Style, state *State) {
|
||||||
|
var (
|
||||||
|
activeStyle = tcell.StyleDefault.Foreground(tcell.GetColor("blue")).Background(tcell.ColorReset)
|
||||||
|
pendingStyle = tcell.StyleDefault.Foreground(tcell.GetColor("green")).Background(tcell.ColorReset)
|
||||||
|
aggregatingStyle = tcell.StyleDefault.Foreground(tcell.GetColor("lightgreen")).Background(tcell.ColorReset)
|
||||||
|
scheduledStyle = tcell.StyleDefault.Foreground(tcell.GetColor("yellow")).Background(tcell.ColorReset)
|
||||||
|
retryStyle = tcell.StyleDefault.Foreground(tcell.GetColor("pink")).Background(tcell.ColorReset)
|
||||||
|
archivedStyle = tcell.StyleDefault.Foreground(tcell.GetColor("purple")).Background(tcell.ColorReset)
|
||||||
|
completedStyle = tcell.StyleDefault.Foreground(tcell.GetColor("darkgreen")).Background(tcell.ColorReset)
|
||||||
|
)
|
||||||
|
|
||||||
|
var qnames []string
|
||||||
|
var qsizes []string // queue size in strings
|
||||||
|
maxSize := 1 // not zero to avoid division by zero
|
||||||
|
for _, q := range state.queues {
|
||||||
|
qnames = append(qnames, q.Queue)
|
||||||
|
qsizes = append(qsizes, strconv.Itoa(q.Size))
|
||||||
|
if q.Size > maxSize {
|
||||||
|
maxSize = q.Size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
qnameWidth := maxwidth(qnames)
|
||||||
|
qsizeWidth := maxwidth(qsizes)
|
||||||
|
|
||||||
|
// Calculate the multipler to scale the graph
|
||||||
|
screenWidth, _ := d.Screen().Size()
|
||||||
|
graphMaxWidth := screenWidth - (qnameWidth + qsizeWidth + 3) // <qname> |<graph> <size>
|
||||||
|
multipiler := 1.0
|
||||||
|
if graphMaxWidth < maxSize {
|
||||||
|
multipiler = float64(graphMaxWidth) / float64(maxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = '▇'
|
||||||
|
for _, q := range state.queues {
|
||||||
|
d.Print(q.Queue, style)
|
||||||
|
d.Print(strings.Repeat(" ", qnameWidth-runewidth.StringWidth(q.Queue)+1), style) // padding between qname and graph
|
||||||
|
d.Print("|", style)
|
||||||
|
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Completed)*multipiler))), completedStyle)
|
||||||
|
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Archived)*multipiler))), archivedStyle)
|
||||||
|
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Retry)*multipiler))), retryStyle)
|
||||||
|
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Scheduled)*multipiler))), scheduledStyle)
|
||||||
|
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Aggregating)*multipiler))), aggregatingStyle)
|
||||||
|
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Pending)*multipiler))), pendingStyle)
|
||||||
|
d.Print(strings.Repeat(string(tick), int(math.Floor(float64(q.Active)*multipiler))), activeStyle)
|
||||||
|
d.Print(fmt.Sprintf(" %d", q.Size), style)
|
||||||
|
d.NL()
|
||||||
|
}
|
||||||
|
d.NL()
|
||||||
|
d.Print("completed=", style)
|
||||||
|
d.Print(string(tick), completedStyle)
|
||||||
|
d.Print(" archived=", style)
|
||||||
|
d.Print(string(tick), archivedStyle)
|
||||||
|
d.Print(" retry=", style)
|
||||||
|
d.Print(string(tick), retryStyle)
|
||||||
|
d.Print(" scheduled=", style)
|
||||||
|
d.Print(string(tick), scheduledStyle)
|
||||||
|
d.Print(" aggregating=", style)
|
||||||
|
d.Print(string(tick), aggregatingStyle)
|
||||||
|
d.Print(" pending=", style)
|
||||||
|
d.Print(string(tick), pendingStyle)
|
||||||
|
d.Print(" active=", style)
|
||||||
|
d.Print(string(tick), activeStyle)
|
||||||
|
d.NL()
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawFooter(d *ScreenDrawer, baseStyle tcell.Style, state *State) {
|
||||||
|
if state.err != nil {
|
||||||
|
style := baseStyle.Background(tcell.ColorDarkRed)
|
||||||
|
d.Print(state.err.Error(), style)
|
||||||
|
d.FillLine(' ', style)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
style := baseStyle.Background(tcell.ColorDarkSlateGray)
|
||||||
|
switch state.view {
|
||||||
|
case viewTypeHelp:
|
||||||
|
d.Print("Esc=GoBack", style)
|
||||||
|
default:
|
||||||
|
type menu struct {
|
||||||
|
label string
|
||||||
|
view viewType
|
||||||
|
}
|
||||||
|
menus := []*menu{
|
||||||
|
{"F1=Queues", viewTypeQueues},
|
||||||
|
{"F2=Servers", viewTypeServers},
|
||||||
|
{"F3=Schedulers", viewTypeSchedulers},
|
||||||
|
{"F4=Redis", viewTypeRedis},
|
||||||
|
{"?=Help", viewTypeHelp},
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for _, m := range menus {
|
||||||
|
b.WriteString(m.label)
|
||||||
|
// Add * for the current view
|
||||||
|
if m.view == state.view {
|
||||||
|
b.WriteString("* ")
|
||||||
|
} else {
|
||||||
|
b.WriteString(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.Print(b.String(), style)
|
||||||
|
}
|
||||||
|
d.FillLine(' ', style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the maximum width from the given list of names
|
||||||
|
func maxwidth(names []string) int {
|
||||||
|
max := 0
|
||||||
|
for _, s := range names {
|
||||||
|
if w := runewidth.StringWidth(s); w > max {
|
||||||
|
max = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteCount converts the given bytes into human readable string
|
||||||
|
func ByteCount(b int64) string {
|
||||||
|
const unit = 1000
|
||||||
|
if b < unit {
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawQueueTable(d *ScreenDrawer, style tcell.Style, state *State) {
|
||||||
|
colConfigs := []*columnConfig[*asynq.QueueInfo]{
|
||||||
|
{"Queue", alignLeft, func(q *asynq.QueueInfo) string { return q.Queue }},
|
||||||
|
{"State", alignLeft, func(q *asynq.QueueInfo) string {
|
||||||
|
if q.Paused {
|
||||||
|
return "PAUSED"
|
||||||
|
} else {
|
||||||
|
return "RUN"
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
{"Size", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Size) }},
|
||||||
|
{"Latency", alignRight, func(q *asynq.QueueInfo) string { return q.Latency.String() }},
|
||||||
|
{"MemoryUsage", alignRight, func(q *asynq.QueueInfo) string { return ByteCount(q.MemoryUsage) }},
|
||||||
|
{"Processed", alignRight, func(q *asynq.QueueInfo) string { return strconv.Itoa(q.Processed) }},
|
||||||
|
{"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 */ }},
|
||||||
|
}
|
||||||
|
drawTable(d, style, colConfigs, state.queues, state.rowIdx-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type columnAlignment int
|
||||||
|
|
||||||
|
const (
|
||||||
|
alignRight columnAlignment = iota
|
||||||
|
alignLeft
|
||||||
|
)
|
||||||
|
|
||||||
|
type columnConfig[V any] struct {
|
||||||
|
name string
|
||||||
|
alignment columnAlignment
|
||||||
|
displayFn func(v V) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type column[V any] struct {
|
||||||
|
*columnConfig[V]
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to draw a table.
|
||||||
|
func drawTable[V any](d *ScreenDrawer, style tcell.Style, configs []*columnConfig[V], data []V, highlightRowIdx int) {
|
||||||
|
const colBuffer = 4 // extra buffer between columns
|
||||||
|
cols := make([]*column[V], len(configs))
|
||||||
|
for i, cfg := range configs {
|
||||||
|
cols[i] = &column[V]{cfg, runewidth.StringWidth(cfg.name)}
|
||||||
|
}
|
||||||
|
// adjust the column width to accommodate the widest value.
|
||||||
|
for _, v := range data {
|
||||||
|
for _, col := range cols {
|
||||||
|
if w := runewidth.StringWidth(col.displayFn(v)); col.width < w {
|
||||||
|
col.width = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// print header
|
||||||
|
headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite)
|
||||||
|
for _, col := range cols {
|
||||||
|
if col.alignment == alignLeft {
|
||||||
|
d.Print(rpad(col.name, col.width+colBuffer), headerStyle)
|
||||||
|
} else {
|
||||||
|
d.Print(lpad(col.name, col.width+colBuffer), headerStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.FillLine(' ', headerStyle)
|
||||||
|
// print body
|
||||||
|
for i, v := range data {
|
||||||
|
rowStyle := style
|
||||||
|
if highlightRowIdx == i {
|
||||||
|
rowStyle = style.Background(tcell.ColorDarkOliveGreen)
|
||||||
|
}
|
||||||
|
for _, col := range cols {
|
||||||
|
if col.alignment == alignLeft {
|
||||||
|
d.Print(rpad(col.displayFn(v), col.width+colBuffer), rowStyle)
|
||||||
|
} else {
|
||||||
|
d.Print(lpad(col.displayFn(v), col.width+colBuffer), rowStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.FillLine(' ', rowStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpad adds padding to the right of a string.
|
||||||
|
func rpad(s string, padding int) string {
|
||||||
|
tmpl := fmt.Sprintf("%%-%ds ", padding)
|
||||||
|
return fmt.Sprintf(tmpl, s)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// lpad adds padding to the left of a string.
|
||||||
|
func lpad(s string, padding int) string {
|
||||||
|
tmpl := fmt.Sprintf("%%%ds ", padding)
|
||||||
|
return fmt.Sprintf(tmpl, s)
|
||||||
|
}
|
82
tools/asynq/cmd/dash/drawer.go
Normal file
82
tools/asynq/cmd/dash/drawer.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package dash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*** Screen Drawer ***/
|
||||||
|
|
||||||
|
// ScreenDrawer is used to draw contents on screen.
|
||||||
|
//
|
||||||
|
// Usage example:
|
||||||
|
// d := NewScreenDrawer(s)
|
||||||
|
// d.Println("Hello world", mystyle)
|
||||||
|
// d.NL() // adds newline
|
||||||
|
// d.Print("foo", mystyle.Bold(true))
|
||||||
|
// d.Print("bar", mystyle.Italic(true))
|
||||||
|
type ScreenDrawer struct {
|
||||||
|
l *LineDrawer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScreenDrawer(s tcell.Screen) *ScreenDrawer {
|
||||||
|
return &ScreenDrawer{l: NewLineDrawer(0, s)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ScreenDrawer) Print(s string, style tcell.Style) {
|
||||||
|
d.l.Draw(s, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ScreenDrawer) Println(s string, style tcell.Style) {
|
||||||
|
d.Print(s, style)
|
||||||
|
d.NL()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillLine prints the given rune until the end of the current line
|
||||||
|
// and adds a newline.
|
||||||
|
func (d *ScreenDrawer) FillLine(r rune, style tcell.Style) {
|
||||||
|
w, _ := d.Screen().Size()
|
||||||
|
s := strings.Repeat(string(r), w-d.l.col)
|
||||||
|
d.Print(s, style)
|
||||||
|
d.NL()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NL adds a newline (i.e., moves to the next line).
|
||||||
|
func (d *ScreenDrawer) NL() {
|
||||||
|
d.l.row++
|
||||||
|
d.l.col = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ScreenDrawer) Screen() tcell.Screen {
|
||||||
|
return d.l.s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to the bottom of the screen.
|
||||||
|
func (d *ScreenDrawer) GoToBottom() {
|
||||||
|
_, h := d.Screen().Size()
|
||||||
|
d.l.row = h - 1
|
||||||
|
d.l.col = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineDrawer struct {
|
||||||
|
s tcell.Screen
|
||||||
|
row int
|
||||||
|
col int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLineDrawer(row int, s tcell.Screen) *LineDrawer {
|
||||||
|
return &LineDrawer{row: row, col: 0, s: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LineDrawer) Draw(s string, style tcell.Style) {
|
||||||
|
for _, r := range s {
|
||||||
|
d.s.SetContent(d.col, d.row, r, nil, style)
|
||||||
|
d.col += runewidth.RuneWidth(r)
|
||||||
|
}
|
||||||
|
}
|
49
tools/asynq/cmd/dash/fetch.go
Normal file
49
tools/asynq/cmd/dash/fetch.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2022 Kentaro Hibino. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package dash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fetchQueueInfo(i *asynq.Inspector, queuesCh chan<- []*asynq.QueueInfo, errorCh chan<- error, opts Options) {
|
||||||
|
if !opts.UseRealData {
|
||||||
|
n := rand.Intn(100)
|
||||||
|
queuesCh <- []*asynq.QueueInfo{
|
||||||
|
{Queue: "default", Size: 1800 + n, Pending: 700 + n, Active: 300, Aggregating: 300, Scheduled: 200, Retry: 100, Archived: 200},
|
||||||
|
{Queue: "critical", Size: 2300 + n, Pending: 1000 + n, Active: 500, Retry: 400, Completed: 400},
|
||||||
|
{Queue: "low", Size: 900 + n, Pending: n, Active: 300, Scheduled: 400, Completed: 200},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queues, err := i.Queues()
|
||||||
|
if err != nil {
|
||||||
|
errorCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var res []*asynq.QueueInfo
|
||||||
|
for _, q := range queues {
|
||||||
|
info, err := i.GetQueueInfo(q)
|
||||||
|
if err != nil {
|
||||||
|
errorCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res = append(res, info)
|
||||||
|
}
|
||||||
|
queuesCh <- res
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchRedisInfo(redisInfoCh chan<- *redisInfo, errorCh chan<- error) {
|
||||||
|
n := rand.Intn(1000)
|
||||||
|
redisInfoCh <- &redisInfo{
|
||||||
|
version: "6.2.6",
|
||||||
|
uptime: "9 days",
|
||||||
|
memoryUsage: n,
|
||||||
|
peakMemoryUsage: n + 123,
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user