410 lines
9.3 KiB
Go
410 lines
9.3 KiB
Go
|
package nmon
|
|||
|
|
|||
|
import (
|
|||
|
"fmt"
|
|||
|
"os"
|
|||
|
"sync"
|
|||
|
"time"
|
|||
|
|
|||
|
"github.com/gdamore/tcell/v2"
|
|||
|
"github.com/rivo/tview"
|
|||
|
"github.com/shirou/gopsutil/v4/cpu"
|
|||
|
"github.com/shirou/gopsutil/v4/disk"
|
|||
|
"github.com/shirou/gopsutil/v4/load"
|
|||
|
"github.com/shirou/gopsutil/v4/mem"
|
|||
|
"github.com/shirou/gopsutil/v4/net"
|
|||
|
"github.com/spf13/cobra"
|
|||
|
)
|
|||
|
|
|||
|
var (
|
|||
|
refreshInterval time.Duration
|
|||
|
savePath string
|
|||
|
app *tview.Application
|
|||
|
showCores bool
|
|||
|
noTUI bool
|
|||
|
diskIOHistory = make(map[string]disk.IOCountersStat)
|
|||
|
netStats = make(map[string]*NetStat)
|
|||
|
fileMutex sync.Mutex
|
|||
|
)
|
|||
|
|
|||
|
type NetStat struct {
|
|||
|
LastBytesSent uint64
|
|||
|
LastBytesRecv uint64
|
|||
|
LastTime time.Time
|
|||
|
CurrentSentRate float64
|
|||
|
CurrentRecvRate float64
|
|||
|
}
|
|||
|
|
|||
|
var Cmd = &cobra.Command{
|
|||
|
Use: "nmon",
|
|||
|
Short: "System Monitoring Tool",
|
|||
|
Run: func(cmd *cobra.Command, args []string) {
|
|||
|
if noTUI {
|
|||
|
runTextMode()
|
|||
|
return
|
|||
|
}
|
|||
|
startTUI()
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
func init() {
|
|||
|
Cmd.Flags().BoolVarP(&noTUI, "no-tui", "t", false, "Run in text mode")
|
|||
|
Cmd.Flags().BoolVarP(&showCores, "cores", "c", false, "Show per-core CPU usage")
|
|||
|
Cmd.Flags().DurationVarP(&refreshInterval, "interval", "i", time.Second, "Refresh interval")
|
|||
|
Cmd.Flags().StringVarP(&savePath, "save", "s", "", "Save monitoring data to file (nmon format)")
|
|||
|
}
|
|||
|
|
|||
|
func main() {
|
|||
|
if err := Cmd.Execute(); err != nil {
|
|||
|
fmt.Println(err)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func createClickableTextView(title string) *tview.TextView {
|
|||
|
view := tview.NewTextView().SetDynamicColors(true)
|
|||
|
view.SetBorder(true).SetTitle(title)
|
|||
|
return view
|
|||
|
}
|
|||
|
|
|||
|
func startTUI() {
|
|||
|
app = tview.NewApplication()
|
|||
|
flex := tview.NewFlex().SetDirection(tview.FlexRow)
|
|||
|
|
|||
|
cpuView := tview.NewTextView().SetDynamicColors(true)
|
|||
|
memView := tview.NewTextView().SetDynamicColors(true)
|
|||
|
netView := tview.NewTextView().SetDynamicColors(true)
|
|||
|
diskView := tview.NewTextView().SetDynamicColors(true)
|
|||
|
|
|||
|
// 动态布局更新函数
|
|||
|
ioStats, _ := disk.IOCounters()
|
|||
|
updateLayout := func() {
|
|||
|
flex.Clear()
|
|||
|
cpuLines := 2 // 固定基础2行(CPU Load + Load Avg)
|
|||
|
if showCores {
|
|||
|
perCPU, _ := cpu.Percent(0, true)
|
|||
|
cpuLines = 2 + len(perCPU)
|
|||
|
}
|
|||
|
flex.AddItem(cpuView, cpuLines, 1, false)
|
|||
|
flex.AddItem(memView, 3, 1, false)
|
|||
|
flex.AddItem(diskView, len(ioStats), 1, false)
|
|||
|
flex.AddItem(netView, 3, 1, false)
|
|||
|
}
|
|||
|
|
|||
|
// 初始化数据
|
|||
|
updateLayout()
|
|||
|
|
|||
|
app.SetMouseCapture(func(event *tcell.EventMouse, action tview.MouseAction) (*tcell.EventMouse, tview.MouseAction) {
|
|||
|
// 示例:打印点击坐标
|
|||
|
if action == tview.MouseLeftClick {
|
|||
|
showCores = !showCores
|
|||
|
app.QueueUpdateDraw(updateLayout)
|
|||
|
}
|
|||
|
return event, action
|
|||
|
})
|
|||
|
|
|||
|
go updateLoop(cpuView, memView, netView, diskView)
|
|||
|
|
|||
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|||
|
if event.Key() == tcell.KeyCtrlC {
|
|||
|
app.Stop()
|
|||
|
}
|
|||
|
return event
|
|||
|
})
|
|||
|
|
|||
|
if err := app.SetRoot(flex, true).Run(); err != nil {
|
|||
|
panic(err)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func updateLoop(cpuView, memView, netView, diskView *tview.TextView) {
|
|||
|
var lastSave time.Time
|
|||
|
for {
|
|||
|
app.QueueUpdateDraw(func() {
|
|||
|
updateCPU(cpuView)
|
|||
|
updateMemory(memView)
|
|||
|
updateNetwork(netView)
|
|||
|
updateDiskIO(diskView)
|
|||
|
})
|
|||
|
|
|||
|
if savePath != "" && time.Since(lastSave) > time.Minute {
|
|||
|
go saveNMONData()
|
|||
|
lastSave = time.Now()
|
|||
|
}
|
|||
|
|
|||
|
time.Sleep(refreshInterval)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func updateCPU(view *tview.TextView) {
|
|||
|
percent, _ := cpu.Percent(time.Second, false)
|
|||
|
loadAvg, _ := load.Avg()
|
|||
|
|
|||
|
text := fmt.Sprintf("[red]CPU Load: [white]%.2f%%\n[red]Load Avg: [white]%.2f, %.2f, %.2f",
|
|||
|
percent[0],
|
|||
|
loadAvg.Load1,
|
|||
|
loadAvg.Load5,
|
|||
|
loadAvg.Load15)
|
|||
|
|
|||
|
if showCores {
|
|||
|
perCPU, _ := cpu.Percent(time.Second, true)
|
|||
|
for i, p := range perCPU {
|
|||
|
text += fmt.Sprintf("\nCore %d: [green]%s[white] %.2f%%",
|
|||
|
i+1,
|
|||
|
progressBar(p, 20),
|
|||
|
p)
|
|||
|
}
|
|||
|
}
|
|||
|
view.SetText(text)
|
|||
|
}
|
|||
|
|
|||
|
func updateMemory(view *tview.TextView) {
|
|||
|
memStat, _ := mem.VirtualMemory()
|
|||
|
text := fmt.Sprintf("[red]Memory: [white]%.2f%% Used (%.2f GB / %.2f GB)",
|
|||
|
memStat.UsedPercent,
|
|||
|
float64(memStat.Used)/1024/1024/1024,
|
|||
|
float64(memStat.Total)/1024/1024/1024)
|
|||
|
view.SetText(text + "\n" + progressBar(memStat.UsedPercent, 50))
|
|||
|
}
|
|||
|
|
|||
|
func updateNetwork(view *tview.TextView) {
|
|||
|
interfaces, _ := net.IOCounters(true)
|
|||
|
now := time.Now()
|
|||
|
var totalSent, totalRecv float64
|
|||
|
|
|||
|
for _, iface := range interfaces {
|
|||
|
if iface.Name == "lo" {
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
stat, exists := netStats[iface.Name]
|
|||
|
if !exists {
|
|||
|
stat = &NetStat{
|
|||
|
LastBytesSent: iface.BytesSent,
|
|||
|
LastBytesRecv: iface.BytesRecv,
|
|||
|
LastTime: now,
|
|||
|
}
|
|||
|
netStats[iface.Name] = stat
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
timeDiff := now.Sub(stat.LastTime).Seconds()
|
|||
|
sentDiff := float64(iface.BytesSent - stat.LastBytesSent)
|
|||
|
recvDiff := float64(iface.BytesRecv - stat.LastBytesRecv)
|
|||
|
|
|||
|
stat.CurrentSentRate = sentDiff / timeDiff
|
|||
|
stat.CurrentRecvRate = recvDiff / timeDiff
|
|||
|
stat.LastBytesSent = iface.BytesSent
|
|||
|
stat.LastBytesRecv = iface.BytesRecv
|
|||
|
stat.LastTime = now
|
|||
|
|
|||
|
totalSent += stat.CurrentSentRate
|
|||
|
totalRecv += stat.CurrentRecvRate
|
|||
|
}
|
|||
|
|
|||
|
view.SetText(fmt.Sprintf("[red]Network: [white]↑%s ↓%s",
|
|||
|
formatSpeed(totalSent),
|
|||
|
formatSpeed(totalRecv)))
|
|||
|
}
|
|||
|
|
|||
|
func updateDiskIO(view *tview.TextView) {
|
|||
|
ioStats, _ := disk.IOCounters()
|
|||
|
text := "[red]Disk I/O:\n"
|
|||
|
|
|||
|
for name, io := range ioStats {
|
|||
|
last, exists := diskIOHistory[name]
|
|||
|
if exists {
|
|||
|
interval := float64(refreshInterval.Seconds())
|
|||
|
readSpeed := float64(io.ReadBytes-last.ReadBytes) / interval
|
|||
|
writeSpeed := float64(io.WriteBytes-last.WriteBytes) / interval
|
|||
|
|
|||
|
text += fmt.Sprintf(" %-8s R:%s W:%s\n",
|
|||
|
name,
|
|||
|
formatSpeed(readSpeed),
|
|||
|
formatSpeed(writeSpeed))
|
|||
|
}
|
|||
|
diskIOHistory[name] = io
|
|||
|
}
|
|||
|
view.SetText(text)
|
|||
|
}
|
|||
|
|
|||
|
func progressBar(percent float64, width int) string {
|
|||
|
filled := int(percent / 100 * float64(width))
|
|||
|
bar := "[green]"
|
|||
|
for i := 0; i < width; i++ {
|
|||
|
if i < filled {
|
|||
|
bar += "■"
|
|||
|
} else {
|
|||
|
bar += "□"
|
|||
|
}
|
|||
|
}
|
|||
|
return bar + "[white]"
|
|||
|
}
|
|||
|
|
|||
|
func formatSpeed(speed float64) string {
|
|||
|
const (
|
|||
|
KB = 1024
|
|||
|
MB = KB * 1024
|
|||
|
)
|
|||
|
|
|||
|
switch {
|
|||
|
case speed >= MB:
|
|||
|
return fmt.Sprintf("%7.3f MB/s", speed/MB)
|
|||
|
case speed >= KB:
|
|||
|
return fmt.Sprintf("%7.3f KB/s", speed/KB)
|
|||
|
default:
|
|||
|
return fmt.Sprintf("%7.3f B/s", speed)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func saveNMONData() {
|
|||
|
fileMutex.Lock()
|
|||
|
defer fileMutex.Unlock()
|
|||
|
|
|||
|
f, err := os.OpenFile(savePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|||
|
if err != nil {
|
|||
|
return
|
|||
|
}
|
|||
|
defer f.Close()
|
|||
|
|
|||
|
now := time.Now().Format("2006-01-02 15:04:05")
|
|||
|
cpuPercent, _ := cpu.Percent(0, false)
|
|||
|
memStat, _ := mem.VirtualMemory()
|
|||
|
ioStats, _ := disk.IOCounters()
|
|||
|
|
|||
|
fmt.Fprintf(f, "%s,CPU_ALL,%.2f\n", now, cpuPercent[0])
|
|||
|
fmt.Fprintf(f, "%s,MEM,%.2f,%.2f\n", now, memStat.UsedPercent, memStat.Available)
|
|||
|
|
|||
|
for name, io := range ioStats {
|
|||
|
fmt.Fprintf(f, "%s,DISKIO,%s,%d,%d\n",
|
|||
|
now, name, io.ReadBytes, io.WriteBytes)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 文本模式相关函数
|
|||
|
func runTextMode() {
|
|||
|
|
|||
|
ticker := time.NewTicker(refreshInterval)
|
|||
|
defer ticker.Stop()
|
|||
|
|
|||
|
for {
|
|||
|
fmt.Println(getCPUInfo())
|
|||
|
fmt.Println(getMemoryInfo())
|
|||
|
fmt.Println(getNetworkInfo())
|
|||
|
fmt.Println(getDiskIOInfo())
|
|||
|
|
|||
|
if savePath != "" {
|
|||
|
go saveNMONData()
|
|||
|
}
|
|||
|
<-ticker.C
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 数据获取函数
|
|||
|
func getCPUInfo() string {
|
|||
|
|
|||
|
percent, _ := cpu.Percent(time.Second, false)
|
|||
|
loadAvg, _ := load.Avg()
|
|||
|
fmt.Print("\033[H\033[2J")
|
|||
|
fmt.Println("=== System Monitor ===")
|
|||
|
text := fmt.Sprintf("[CPU]\nLoad: %.2f%% Avg: %.2f, %.2f, %.2f",
|
|||
|
percent[0],
|
|||
|
loadAvg.Load1,
|
|||
|
loadAvg.Load5,
|
|||
|
loadAvg.Load15)
|
|||
|
|
|||
|
if showCores {
|
|||
|
perCPU, _ := cpu.Percent(time.Second, true)
|
|||
|
for i, p := range perCPU {
|
|||
|
text += fmt.Sprintf("\nCore %d: %s %.2f%%",
|
|||
|
i+1,
|
|||
|
textProgressBar(p, 20),
|
|||
|
p)
|
|||
|
}
|
|||
|
}
|
|||
|
return text
|
|||
|
}
|
|||
|
|
|||
|
func getMemoryInfo() string {
|
|||
|
memStat, _ := mem.VirtualMemory()
|
|||
|
return fmt.Sprintf("[Memory]\nUsed: %.2f%% (%.2fG/%.2fG)\n%s",
|
|||
|
memStat.UsedPercent,
|
|||
|
float64(memStat.Used)/1024/1024/1024,
|
|||
|
float64(memStat.Total)/1024/1024/1024,
|
|||
|
textProgressBar(memStat.UsedPercent, 50))
|
|||
|
}
|
|||
|
|
|||
|
func getNetworkInfo() string {
|
|||
|
interfaces, _ := net.IOCounters(true)
|
|||
|
now := time.Now()
|
|||
|
var sent, recv float64
|
|||
|
|
|||
|
for _, iface := range interfaces {
|
|||
|
if iface.Name == "lo" {
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
stat, exists := netStats[iface.Name]
|
|||
|
if !exists {
|
|||
|
stat = &NetStat{
|
|||
|
LastBytesSent: iface.BytesSent,
|
|||
|
LastBytesRecv: iface.BytesRecv,
|
|||
|
LastTime: now,
|
|||
|
}
|
|||
|
netStats[iface.Name] = stat
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
timeDiff := now.Sub(stat.LastTime).Seconds()
|
|||
|
sentDiff := float64(iface.BytesSent - stat.LastBytesSent)
|
|||
|
recvDiff := float64(iface.BytesRecv - stat.LastBytesRecv)
|
|||
|
|
|||
|
stat.CurrentSentRate = sentDiff / timeDiff
|
|||
|
stat.CurrentRecvRate = recvDiff / timeDiff
|
|||
|
stat.LastBytesSent = iface.BytesSent
|
|||
|
stat.LastBytesRecv = iface.BytesRecv
|
|||
|
stat.LastTime = now
|
|||
|
|
|||
|
sent += stat.CurrentSentRate
|
|||
|
recv += stat.CurrentRecvRate
|
|||
|
}
|
|||
|
|
|||
|
return fmt.Sprintf("[Network]\nUpload: %s\nDownload: %s",
|
|||
|
formatSpeed(sent),
|
|||
|
formatSpeed(recv))
|
|||
|
}
|
|||
|
|
|||
|
func getDiskIOInfo() string {
|
|||
|
ioStats, _ := disk.IOCounters()
|
|||
|
text := "[Disk I/O]"
|
|||
|
|
|||
|
for name, io := range ioStats {
|
|||
|
last, exists := diskIOHistory[name]
|
|||
|
if exists {
|
|||
|
interval := float64(refreshInterval.Seconds())
|
|||
|
read := float64(io.ReadBytes-last.ReadBytes) / interval
|
|||
|
write := float64(io.WriteBytes-last.WriteBytes) / interval
|
|||
|
|
|||
|
text += fmt.Sprintf("\n%-8s Read: %s Write: %s",
|
|||
|
name,
|
|||
|
formatSpeed(read),
|
|||
|
formatSpeed(write))
|
|||
|
}
|
|||
|
diskIOHistory[name] = io
|
|||
|
}
|
|||
|
return text
|
|||
|
}
|
|||
|
|
|||
|
func textProgressBar(percent float64, width int) string {
|
|||
|
filled := int(percent / 100 * float64(width))
|
|||
|
bar := ""
|
|||
|
for i := 0; i < width; i++ {
|
|||
|
if i < filled {
|
|||
|
bar += "■"
|
|||
|
} else {
|
|||
|
bar += "□"
|
|||
|
}
|
|||
|
}
|
|||
|
return bar
|
|||
|
}
|