-
Notifications
You must be signed in to change notification settings - Fork 3
feat(cmd): top boards #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
a0647f9
c870c09
81ce1ce
899d8a5
4c0fbce
970b020
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,308 @@ | ||||||
| package main | ||||||
|
|
||||||
| import ( | ||||||
| "bufio" | ||||||
| "encoding/json" | ||||||
| "flag" | ||||||
| "fmt" | ||||||
| "log" | ||||||
| "net/http" | ||||||
| "strings" | ||||||
| "sync" | ||||||
| "sync/atomic" | ||||||
| "time" | ||||||
|
|
||||||
| "github.com/dustin/go-humanize" | ||||||
| terminal "github.com/gizak/termui/v3" | ||||||
| "github.com/gizak/termui/v3/widgets" | ||||||
| "github.com/samber/lo" | ||||||
| ) | ||||||
|
|
||||||
| var ( | ||||||
| endpoint = "" | ||||||
| tickInterval = time.Second * 1 | ||||||
| ) | ||||||
|
|
||||||
| func init() { | ||||||
| flag.StringVar(&endpoint, "endpoint", "http://localhost:8080/plugin/qs/graph", "The metrics endpoint to fetch data from tavern server.") | ||||||
| flag.DurationVar(&tickInterval, "interval", time.Second*1, "The interval to fetch metrics.") | ||||||
| } | ||||||
|
|
||||||
| func main() { | ||||||
| flag.Parse() | ||||||
|
|
||||||
| newDashboard() | ||||||
| } | ||||||
|
|
||||||
| func newDashboard() { | ||||||
| if err := terminal.Init(); err != nil { | ||||||
| log.Fatalf("failed to initialize termui: %v", err) | ||||||
| } | ||||||
| defer terminal.Close() | ||||||
|
|
||||||
| termWidth, _ := terminal.TerminalDimensions() | ||||||
|
|
||||||
| collected := atomic.Bool{} | ||||||
| cpuPercent := atomic.Uint32{} | ||||||
| memUsage := atomic.Uint64{} | ||||||
| memTotal := atomic.Uint64{} | ||||||
| diskPercent := atomic.Uint64{} // mock | ||||||
| diskUsage := atomic.Uint64{} | ||||||
| diskTotal := atomic.Uint64{} | ||||||
| startedAt := atomic.Int64{} | ||||||
|
|
||||||
| // 高级监控指标 { 热点url 热点域名 热点磁盘 } | ||||||
| list := widgets.NewList() | ||||||
| list.Title = "Hot URLs" | ||||||
| list.SetRect(0, 12, termWidth, 30) | ||||||
| list.BorderStyle.Fg = terminal.ColorWhite | ||||||
| list.TitleStyle.Fg = terminal.ColorCyan | ||||||
| list.TextStyle.Fg = terminal.ColorYellow | ||||||
|
|
||||||
| client := &http.Client{ | ||||||
| Transport: &http.Transport{}, | ||||||
| } | ||||||
|
|
||||||
| var ( | ||||||
| dataMu sync.RWMutex | ||||||
| latestData = make(map[string]float64) | ||||||
| latestHotUrls []string | ||||||
| ) | ||||||
|
|
||||||
| // Background SSE consumer | ||||||
| go func() { | ||||||
| for { | ||||||
| func() { | ||||||
| req, err := http.NewRequest(http.MethodGet, endpoint, nil) | ||||||
| if err != nil { | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| resp, err := client.Do(req) | ||||||
| if err != nil { | ||||||
| collected.Store(false) | ||||||
| return | ||||||
| } | ||||||
| defer resp.Body.Close() | ||||||
|
|
||||||
| if resp.StatusCode != http.StatusOK { | ||||||
| collected.Store(false) | ||||||
| return | ||||||
| } | ||||||
|
|
||||||
| collected.Store(true) | ||||||
| reader := bufio.NewReader(resp.Body) | ||||||
| for { | ||||||
| line, err := reader.ReadString('\n') | ||||||
| if err != nil { | ||||||
| return | ||||||
| } | ||||||
| line = strings.TrimSpace(line) | ||||||
| if strings.HasPrefix(line, "data:") { | ||||||
| jsonStr := strings.TrimSpace(strings.TrimPrefix(line, "data:")) | ||||||
| if jsonStr == "" { | ||||||
| continue | ||||||
| } | ||||||
|
|
||||||
| var rsp Graph | ||||||
| if err := json.Unmarshal([]byte(jsonStr), &rsp); err == nil { | ||||||
| dataMu.Lock() | ||||||
| latestData = rsp.Data | ||||||
| latestHotUrls = rsp.HotUrls | ||||||
| dataMu.Unlock() | ||||||
|
|
||||||
| startedAt.Store(rsp.StartedAt) | ||||||
| cpuPercent.Store(uint32(rsp.Data["cpu_percent"])) | ||||||
| memUsage.Store(uint64(rsp.Data["mem_usage"])) | ||||||
| memTotal.Store(uint64(rsp.Data["mem_total"])) | ||||||
| diskUsage.Store(uint64(rsp.Data["disk_usage"])) | ||||||
| diskTotal.Store(uint64(rsp.Data["disk_total"])) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| }() | ||||||
| time.Sleep(time.Second) // Reconnect delay | ||||||
| } | ||||||
| }() | ||||||
|
|
||||||
| // 基础监控指标 { qps, cpu, memory } | ||||||
| metricGrid := terminal.NewGrid() | ||||||
| metricGrid.SetRect(0, 3, termWidth, 20) | ||||||
|
|
||||||
| banner, bannerDraw := func() (*widgets.Paragraph, func()) { | ||||||
| banner := widgets.NewParagraph() | ||||||
| banner.SetRect(0, 0, termWidth, 3) | ||||||
| banner.Title = " Tavern (PRESS q TO QUIT) " | ||||||
| banner.Border = true | ||||||
|
|
||||||
| textDraw := func() { | ||||||
| color := "fg:red" | ||||||
| status := "Disconnected" | ||||||
| if collected.Load() { | ||||||
| color = "fg:green" | ||||||
| status = "Connected" | ||||||
| } | ||||||
|
|
||||||
| startAt := time.UnixMilli(startedAt.Load()) | ||||||
|
|
||||||
| banner.Text = fmt.Sprintf("%s | Sampling @ [%s](fg:blue) | [%s](%s) (%s) | Uptime %s", | ||||||
| endpoint, tickInterval.String(), status, color, startAt.Format(time.RFC1123), humanize.Time(startAt)) | ||||||
| } | ||||||
| textDraw() | ||||||
|
|
||||||
| return banner, textDraw | ||||||
| }() | ||||||
|
|
||||||
| rater, raterDraw := func() (*widgets.Paragraph, func()) { | ||||||
| rater := widgets.NewParagraph() | ||||||
| rater.Title = "Requests" | ||||||
| rater.SetRect(0, 3, 50, 6) | ||||||
| rater.BorderStyle.Fg = terminal.ColorWhite | ||||||
| rater.TitleStyle.Fg = terminal.ColorCyan | ||||||
|
|
||||||
| draw := func() { | ||||||
| dataMu.RLock() | ||||||
| data := make(map[string]float64, len(latestData)) | ||||||
| for k, v := range latestData { | ||||||
| data[k] = v | ||||||
| } | ||||||
| hotUrls := make([]string, len(latestHotUrls)) | ||||||
| copy(hotUrls, latestHotUrls) | ||||||
| dataMu.RUnlock() | ||||||
|
|
||||||
| rater.Text = fmt.Sprintf("\nRequests/sec: %d \nTotal: %d \n2xx : %d\n4xx : %d\n499 : %d\n5xx : %d", | ||||||
| int(data["total"]), int(data["total"]), int(data["2xx"]), int(data["4xx"]), int(data["499"]), int(data["5xx"])) | ||||||
|
||||||
| int(data["total"]), int(data["total"]), int(data["2xx"]), int(data["4xx"]), int(data["499"]), int(data["5xx"])) | |
| int(data["rps"]), int(data["total"]), int(data["2xx"]), int(data["4xx"]), int(data["499"]), int(data["5xx"])) |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
diskPercent is never set (and the label hardcodes 0%%), so the Disk gauge percent will always be 0 regardless of actual usage. Either compute and store the percentage from disk_usage/disk_total, or remove diskPercent and derive percent in diskDraw.
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The render loop uses time.NewTicker(time.Second) and ignores the -interval flag, so UI refresh rate can't be configured. Use tickInterval when creating the ticker (and stop it on exit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Bucket.TopKdoc says it returns "the top k most frequently used keys", but current implementations return formatted strings likepath@@time@@refsfor UI consumption. This mismatch makes the interface confusing and brittle. Consider either (1) changing the method name/docs to reflect returning display/metadata strings, or (2) returning a structured type (e.g.[]HotKey) and/or separateTopKKeys/TopKStatsAPIs.