mirror of
https://github.com/hibiken/asynq.git
synced 2025-09-19 13:21:58 +08:00
(cli): Add dash command
This commit is contained in:
490
tools/asynq/cmd/dash.go
Normal file
490
tools/asynq/cmd/dash.go
Normal file
@@ -0,0 +1,490 @@
|
||||
// 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var dashCmd = &cobra.Command{
|
||||
Use: "dash",
|
||||
Short: "View dashboard",
|
||||
Long: heredoc.Doc(`
|
||||
Displays dashboard.`),
|
||||
Args: cobra.NoArgs,
|
||||
Run: dash,
|
||||
}
|
||||
|
||||
var (
|
||||
flagDebug = false
|
||||
flagUseRealData = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(dashCmd)
|
||||
// TODO: Remove this debug once we're done
|
||||
dashCmd.Flags().BoolVar(&flagDebug, "debug", false, "Print debug info")
|
||||
dashCmd.Flags().BoolVar(&flagUseRealData, "realdata", false, "Use real data in redis")
|
||||
}
|
||||
|
||||
type dashState struct {
|
||||
queues []*asynq.QueueInfo
|
||||
err error
|
||||
rowIdx int // highlighted row
|
||||
}
|
||||
|
||||
func dash(cmd *cobra.Command, args []string) {
|
||||
s, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
fmt.Println("failed to create a screen: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := s.Init(); err != nil {
|
||||
fmt.Println("failed to initialize screen: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inspector := asynq.NewInspector(asynq.RedisClientOpt{Addr: ":6379"})
|
||||
|
||||
// Set default text style
|
||||
baseStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
|
||||
s.SetStyle(baseStyle)
|
||||
|
||||
queues, err := getQueueInfo(inspector)
|
||||
state := dashState{
|
||||
queues: queues,
|
||||
err: err,
|
||||
}
|
||||
// draw initial screen
|
||||
drawDash(s, baseStyle, &state)
|
||||
|
||||
eventCh := make(chan tcell.Event)
|
||||
done := make(chan struct{})
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// TODO: Double check that we are not leaking goroutine with this one.
|
||||
go s.ChannelEvents(eventCh, done)
|
||||
|
||||
quit := func() {
|
||||
s.Fini()
|
||||
close(done)
|
||||
os.Exit(0)
|
||||
}
|
||||
for {
|
||||
// Update screen
|
||||
s.Show()
|
||||
|
||||
select {
|
||||
case ev := <-eventCh:
|
||||
// Process event
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
s.Sync()
|
||||
case *tcell.EventKey:
|
||||
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC || ev.Rune() == 'q' {
|
||||
quit()
|
||||
} else if ev.Key() == tcell.KeyCtrlL {
|
||||
s.Sync()
|
||||
} else if ev.Key() == tcell.KeyDown || ev.Rune() == 'j' {
|
||||
if state.rowIdx < len(state.queues) {
|
||||
state.rowIdx++
|
||||
} else {
|
||||
state.rowIdx = 0 // loop back
|
||||
}
|
||||
drawDash(s, baseStyle, &state)
|
||||
} else if ev.Key() == tcell.KeyUp || ev.Rune() == 'k' {
|
||||
if state.rowIdx == 0 {
|
||||
state.rowIdx = len(state.queues)
|
||||
} else {
|
||||
state.rowIdx--
|
||||
}
|
||||
drawDash(s, baseStyle, &state)
|
||||
}
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
state.queues, state.err = getQueueInfo(inspector)
|
||||
drawDash(s, baseStyle, &state)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getQueueInfo(i *asynq.Inspector) ([]*asynq.QueueInfo, error) {
|
||||
if !flagUseRealData {
|
||||
n := rand.Intn(100)
|
||||
return []*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},
|
||||
}, nil
|
||||
}
|
||||
queues, err := i.Queues()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res []*asynq.QueueInfo
|
||||
for _, q := range queues {
|
||||
info, err := i.GetQueueInfo(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, info)
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
func drawDash(s tcell.Screen, style tcell.Style, state *dashState) {
|
||||
s.Clear()
|
||||
// Simulate data update on every render
|
||||
d := NewScreenDrawer(s)
|
||||
d.Println("=== Queues ===", style.Bold(true))
|
||||
d.NL() // empty line
|
||||
drawQueueSizeGraphs(d, style, state)
|
||||
d.NL() // empty line
|
||||
drawQueueTable(d, style, state)
|
||||
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)
|
||||
d.Print("F1=HELP", 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
|
||||
}
|
||||
|
||||
const colBuffer = 4 // extra buffer between columns
|
||||
|
||||
type columnAlignment int
|
||||
|
||||
const (
|
||||
alignRight columnAlignment = iota
|
||||
alignLeft
|
||||
)
|
||||
|
||||
type column struct {
|
||||
name string
|
||||
width int
|
||||
align columnAlignment
|
||||
displayValues []string // TODO: Can we use these displayValues to display stuff?
|
||||
}
|
||||
|
||||
func newColumn(name string, align columnAlignment) *column {
|
||||
return &column{
|
||||
name: name,
|
||||
width: runewidth.StringWidth(name),
|
||||
align: align,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *column) accommodate(v string) {
|
||||
c.displayValues = append(c.displayValues, v)
|
||||
if w := runewidth.StringWidth(v); w > c.width {
|
||||
c.width = w
|
||||
}
|
||||
}
|
||||
|
||||
type table struct {
|
||||
cols []*column
|
||||
}
|
||||
|
||||
// QueueInfoFormatter exposes API to return display values for QueueInfo properties.
|
||||
type QueueInfoFormatter struct {
|
||||
q *asynq.QueueInfo
|
||||
}
|
||||
|
||||
func (f *QueueInfoFormatter) Queue() string { return f.q.Queue }
|
||||
func (f *QueueInfoFormatter) Size() string { return strconv.Itoa(f.q.Size) }
|
||||
func (f *QueueInfoFormatter) Processed() string { return strconv.Itoa(f.q.Processed) }
|
||||
func (f *QueueInfoFormatter) Failed() string { return strconv.Itoa(f.q.Failed) }
|
||||
|
||||
func (f *QueueInfoFormatter) State() string {
|
||||
if f.q.Paused {
|
||||
return "PAUSED"
|
||||
}
|
||||
return "RUN"
|
||||
}
|
||||
|
||||
func (f *QueueInfoFormatter) Latency() string {
|
||||
return f.q.Latency.String()
|
||||
}
|
||||
|
||||
func (f *QueueInfoFormatter) ErrorRate() string {
|
||||
return "0.23%" // TODO: Implement this
|
||||
}
|
||||
|
||||
func (f *QueueInfoFormatter) MemoryUsage() string {
|
||||
return ByteCount(f.q.MemoryUsage)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
columns := []*column{
|
||||
newColumn("Queue", alignLeft),
|
||||
newColumn("State", alignLeft),
|
||||
newColumn("Size", alignRight),
|
||||
newColumn("Latency", alignRight),
|
||||
newColumn("MemoryUsage", alignRight),
|
||||
newColumn("Processed", alignRight),
|
||||
newColumn("Failed", alignRight),
|
||||
newColumn("ErrorRate", alignRight),
|
||||
}
|
||||
|
||||
// Adjust the column widths to accomodate the values
|
||||
for _, q := range state.queues {
|
||||
f := QueueInfoFormatter{q}
|
||||
for _, col := range columns {
|
||||
switch col.name {
|
||||
case "Queue":
|
||||
col.accommodate(f.Queue())
|
||||
case "State":
|
||||
col.accommodate(f.State())
|
||||
case "Size":
|
||||
col.accommodate(f.Size())
|
||||
case "MemoryUsage":
|
||||
col.accommodate(f.MemoryUsage())
|
||||
case "Latency":
|
||||
col.accommodate(f.Latency())
|
||||
case "Processed":
|
||||
col.accommodate(f.Processed())
|
||||
case "Failed":
|
||||
col.accommodate(f.Failed())
|
||||
case "ErrorRate":
|
||||
col.accommodate(f.ErrorRate())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header
|
||||
headerStyle := style.Background(tcell.ColorDimGray).Foreground(tcell.ColorWhite)
|
||||
width, _ := d.Screen().Size()
|
||||
var b strings.Builder
|
||||
for _, col := range columns {
|
||||
if col.align == alignRight {
|
||||
b.WriteString(lpad(col.name, col.width+colBuffer))
|
||||
} else {
|
||||
b.WriteString(rpad(col.name, col.width+colBuffer))
|
||||
}
|
||||
}
|
||||
b.WriteString(strings.Repeat(" ", width-b.Len())) // span the full width
|
||||
d.Println(b.String(), headerStyle)
|
||||
|
||||
// Body
|
||||
for i, q := range state.queues {
|
||||
rowStyle := style
|
||||
if state.rowIdx == i+1 {
|
||||
rowStyle = style.Background(tcell.ColorDarkOliveGreen)
|
||||
}
|
||||
f := QueueInfoFormatter{q}
|
||||
for _, col := range columns {
|
||||
switch col.name {
|
||||
case "Queue":
|
||||
d.Print(rpad(f.Queue(), col.width+colBuffer), rowStyle)
|
||||
case "State":
|
||||
d.Print(rpad(f.State(), col.width+colBuffer), rowStyle)
|
||||
case "Size":
|
||||
d.Print(lpad(f.Size(), col.width+colBuffer), rowStyle)
|
||||
case "MemoryUsage":
|
||||
d.Print(lpad(f.MemoryUsage(), col.width+colBuffer), rowStyle)
|
||||
case "Latency":
|
||||
d.Print(lpad(f.Latency(), col.width+colBuffer), rowStyle)
|
||||
case "Processed":
|
||||
d.Print(lpad(f.Processed(), col.width+colBuffer), rowStyle)
|
||||
case "Failed":
|
||||
d.Print(lpad(f.Failed(), col.width+colBuffer), rowStyle)
|
||||
case "ErrorRate":
|
||||
d.Print(lpad(f.ErrorRate(), col.width+colBuffer), rowStyle)
|
||||
}
|
||||
}
|
||||
d.FillLine(' ', rowStyle)
|
||||
}
|
||||
|
||||
if flagDebug {
|
||||
d.Println(fmt.Sprintf("DEBUG: rowIdx = %d", state.rowIdx), style)
|
||||
}
|
||||
}
|
||||
|
||||
/*** 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 run 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)
|
||||
}
|
||||
}
|
@@ -259,6 +259,12 @@ func rpad(s string, padding int) string {
|
||||
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// indent indents the given text by given spaces.
|
||||
func indent(text string, space int) string {
|
||||
if len(text) == 0 {
|
||||
|
@@ -5,9 +5,12 @@ go 1.13
|
||||
require (
|
||||
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/gdamore/tcell v1.4.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.5.1 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/hibiken/asynq v0.23.0
|
||||
github.com/hibiken/asynq/x v0.0.0-20220131170841-349f4c50fb1d
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/spf13/cobra v1.1.1
|
||||
|
23
tools/go.sum
23
tools/go.sum
@@ -59,6 +59,12 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||
github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I=
|
||||
github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@@ -169,6 +175,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
@@ -178,6 +188,10 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
@@ -244,6 +258,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
@@ -372,6 +388,7 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -387,13 +404,19 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
Reference in New Issue
Block a user