package main import ( "fmt" "log" "net/http" "sort" "strings" "time" "github.com/gdamore/tcell/v2" "github.com/pelletier/go-toml" "github.com/prometheus/common/expfmt" "github.com/rivo/tview" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v3/process" ) var prometheusURL string func init() { configPaths := []string{ "/etc/hmac-file-server/config.toml", "../config.toml", } var config *toml.Tree var err error for _, path := range configPaths { config, err = toml.LoadFile(path) if err == nil { break } } if err != nil { log.Fatalf("Error loading config file: %v", err) } port := config.Get("server.metrics_port").(int64) prometheusURL = fmt.Sprintf("http://localhost:%d/metrics", port) } // Thresholds for color coding const ( HighUsage = 80.0 MediumUsage = 50.0 ) // ProcessInfo holds information about a process type ProcessInfo struct { PID int32 Name string CPUPercent float64 MemPercent float32 CommandLine string } // Function to fetch and parse Prometheus metrics func fetchMetrics() (map[string]float64, error) { resp, err := http.Get(prometheusURL) if err != nil { return nil, fmt.Errorf("failed to fetch metrics: %w", err) } defer resp.Body.Close() parser := &expfmt.TextParser{} metricFamilies, err := parser.TextToMetricFamilies(resp.Body) if err != nil { return nil, fmt.Errorf("failed to parse metrics: %w", err) } metrics := make(map[string]float64) for name, mf := range metricFamilies { // Filter the metrics you're interested in if strings.HasPrefix(name, "hmac_file_server_") || name == "memory_usage_bytes" || name == "cpu_usage_percent" || name == "active_connections_total" || name == "goroutines_count" { for _, m := range mf.GetMetric() { var value float64 if m.GetGauge() != nil { value = m.GetGauge().GetValue() } else if m.GetCounter() != nil { value = m.GetCounter().GetValue() } else if m.GetUntyped() != nil { value = m.GetUntyped().GetValue() } else { // If the metric type is not handled, skip it continue } // Handle metrics with labels if len(m.GetLabel()) > 0 { labels := make([]string, 0) for _, label := range m.GetLabel() { labels = append(labels, fmt.Sprintf("%s=\"%s\"", label.GetName(), label.GetValue())) } metricKey := fmt.Sprintf("%s{%s}", name, strings.Join(labels, ",")) metrics[metricKey] = value } else { metrics[name] = value } } } } return metrics, nil } // Function to fetch system data func fetchSystemData() (float64, float64, int, error) { v, err := mem.VirtualMemory() if err != nil { return 0, 0, 0, fmt.Errorf("failed to fetch memory data: %w", err) } c, err := cpu.Percent(0, false) if err != nil { return 0, 0, 0, fmt.Errorf("failed to fetch CPU data: %w", err) } cores, err := cpu.Counts(true) if err != nil { return 0, 0, 0, fmt.Errorf("failed to fetch CPU cores: %w", err) } cpuUsage := 0.0 if len(c) > 0 { cpuUsage = c[0] } return v.UsedPercent, cpuUsage, cores, nil } // Function to fetch process list func fetchProcessList() ([]ProcessInfo, error) { processes, err := process.Processes() if err != nil { return nil, fmt.Errorf("failed to fetch processes: %w", err) } var processList []ProcessInfo for _, p := range processes { cpuPercent, err := p.CPUPercent() if err != nil { continue } memPercent, err := p.MemoryPercent() if err != nil { continue } name, err := p.Name() if err != nil { continue } cmdline, err := p.Cmdline() if err != nil { cmdline = "" } processList = append(processList, ProcessInfo{ PID: p.Pid, Name: name, CPUPercent: cpuPercent, MemPercent: memPercent, CommandLine: cmdline, }) } return processList, nil } // Function to fetch detailed information about hmac-file-server func fetchHmacFileServerInfo() (*ProcessInfo, error) { processes, err := process.Processes() if err != nil { return nil, fmt.Errorf("failed to fetch processes: %w", err) } for _, p := range processes { name, err := p.Name() if err != nil { continue } if name == "hmac-file-server" { cpuPercent, err := p.CPUPercent() if err != nil { cpuPercent = 0.0 } memPercent, err := p.MemoryPercent() if err != nil { memPercent = 0.0 } cmdline, err := p.Cmdline() if err != nil { cmdline = "" } return &ProcessInfo{ PID: p.Pid, Name: name, CPUPercent: cpuPercent, MemPercent: memPercent, CommandLine: cmdline, }, nil } } return nil, fmt.Errorf("hmac-file-server process not found") } // Function to update the UI with the latest data func updateUI(app *tview.Application, pages *tview.Pages, sysPage, hmacPage tview.Primitive) { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() for range ticker.C { // Fetch data for both views memUsage, cpuUsage, cores, err := fetchSystemData() if err != nil { log.Printf("Error fetching system data: %v\n", err) continue } metrics, err := fetchMetrics() if err != nil { log.Printf("Error fetching metrics: %v\n", err) continue } processes, err := fetchProcessList() if err != nil { log.Printf("Error fetching process list: %v\n", err) continue } hmacInfo, err := fetchHmacFileServerInfo() if err != nil { log.Printf("Error fetching hmac-file-server info: %v\n", err) } // Update the UI app.QueueUpdateDraw(func() { // Update system page if currentPage, _ := pages.GetFrontPage(); currentPage == "system" { sysFlex := sysPage.(*tview.Flex) // Update system data table sysTable := sysFlex.GetItem(0).(*tview.Table) updateSystemTable(sysTable, memUsage, cpuUsage, cores) // Update metrics table metricsTable := sysFlex.GetItem(1).(*tview.Table) updateMetricsTable(metricsTable, metrics) // Update process table processTable := sysFlex.GetItem(2).(*tview.Table) updateProcessTable(processTable, processes) } // Update hmac-file-server page if currentPage, _ := pages.GetFrontPage(); currentPage == "hmac" && hmacInfo != nil { hmacFlex := hmacPage.(*tview.Flex) hmacTable := hmacFlex.GetItem(0).(*tview.Table) updateHmacTable(hmacTable, hmacInfo, metrics) } }) } } // Helper function to update system data table func updateSystemTable(sysTable *tview.Table, memUsage, cpuUsage float64, cores int) { sysTable.Clear() sysTable.SetCell(0, 0, tview.NewTableCell("Metric").SetAttributes(tcell.AttrBold)) sysTable.SetCell(0, 1, tview.NewTableCell("Value").SetAttributes(tcell.AttrBold)) // CPU Usage Row cpuUsageCell := tview.NewTableCell(fmt.Sprintf("%.2f%%", cpuUsage)) if cpuUsage > HighUsage { cpuUsageCell.SetTextColor(tcell.ColorRed) } else if cpuUsage > MediumUsage { cpuUsageCell.SetTextColor(tcell.ColorYellow) } else { cpuUsageCell.SetTextColor(tcell.ColorGreen) } sysTable.SetCell(1, 0, tview.NewTableCell("CPU Usage")) sysTable.SetCell(1, 1, cpuUsageCell) // Memory Usage Row memUsageCell := tview.NewTableCell(fmt.Sprintf("%.2f%%", memUsage)) if memUsage > HighUsage { memUsageCell.SetTextColor(tcell.ColorRed) } else if memUsage > MediumUsage { memUsageCell.SetTextColor(tcell.ColorYellow) } else { memUsageCell.SetTextColor(tcell.ColorGreen) } sysTable.SetCell(2, 0, tview.NewTableCell("Memory Usage")) sysTable.SetCell(2, 1, memUsageCell) // CPU Cores Row sysTable.SetCell(3, 0, tview.NewTableCell("CPU Cores")) sysTable.SetCell(3, 1, tview.NewTableCell(fmt.Sprintf("%d", cores))) } // Helper function to update metrics table func updateMetricsTable(metricsTable *tview.Table, metrics map[string]float64) { metricsTable.Clear() metricsTable.SetCell(0, 0, tview.NewTableCell("Metric").SetAttributes(tcell.AttrBold)) metricsTable.SetCell(0, 1, tview.NewTableCell("Value").SetAttributes(tcell.AttrBold)) row := 1 for key, value := range metrics { metricsTable.SetCell(row, 0, tview.NewTableCell(key)) metricsTable.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("%.2f", value))) row++ } } // Helper function to update process table func updateProcessTable(processTable *tview.Table, processes []ProcessInfo) { processTable.Clear() processTable.SetCell(0, 0, tview.NewTableCell("PID").SetAttributes(tcell.AttrBold)) processTable.SetCell(0, 1, tview.NewTableCell("Name").SetAttributes(tcell.AttrBold)) processTable.SetCell(0, 2, tview.NewTableCell("CPU%").SetAttributes(tcell.AttrBold)) processTable.SetCell(0, 3, tview.NewTableCell("Mem%").SetAttributes(tcell.AttrBold)) processTable.SetCell(0, 4, tview.NewTableCell("Command").SetAttributes(tcell.AttrBold)) // Sort processes by CPU usage sort.Slice(processes, func(i, j int) bool { return processes[i].CPUPercent > processes[j].CPUPercent }) // Limit to top 20 processes maxRows := 20 if len(processes) < maxRows { maxRows = len(processes) } for i := 0; i < maxRows; i++ { p := processes[i] processTable.SetCell(i+1, 0, tview.NewTableCell(fmt.Sprintf("%d", p.PID))) processTable.SetCell(i+1, 1, tview.NewTableCell(p.Name)) processTable.SetCell(i+1, 2, tview.NewTableCell(fmt.Sprintf("%.2f", p.CPUPercent))) processTable.SetCell(i+1, 3, tview.NewTableCell(fmt.Sprintf("%.2f", p.MemPercent))) processTable.SetCell(i+1, 4, tview.NewTableCell(p.CommandLine)) } } // Helper function to update hmac-table func updateHmacTable(hmacTable *tview.Table, hmacInfo *ProcessInfo, metrics map[string]float64) { hmacTable.Clear() hmacTable.SetCell(0, 0, tview.NewTableCell("Property").SetAttributes(tcell.AttrBold)) hmacTable.SetCell(0, 1, tview.NewTableCell("Value").SetAttributes(tcell.AttrBold)) // Process information hmacTable.SetCell(1, 0, tview.NewTableCell("PID")) hmacTable.SetCell(1, 1, tview.NewTableCell(fmt.Sprintf("%d", hmacInfo.PID))) hmacTable.SetCell(2, 0, tview.NewTableCell("CPU%")) hmacTable.SetCell(2, 1, tview.NewTableCell(fmt.Sprintf("%.2f", hmacInfo.CPUPercent))) hmacTable.SetCell(3, 0, tview.NewTableCell("Mem%")) hmacTable.SetCell(3, 1, tview.NewTableCell(fmt.Sprintf("%.2f", hmacInfo.MemPercent))) hmacTable.SetCell(4, 0, tview.NewTableCell("Command")) hmacTable.SetCell(4, 1, tview.NewTableCell(hmacInfo.CommandLine)) // Metrics related to hmac-file-server row := 6 hmacTable.SetCell(row, 0, tview.NewTableCell("Metric").SetAttributes(tcell.AttrBold)) hmacTable.SetCell(row, 1, tview.NewTableCell("Value").SetAttributes(tcell.AttrBold)) row++ for key, value := range metrics { if strings.Contains(key, "hmac_file_server_") { hmacTable.SetCell(row, 0, tview.NewTableCell(key)) hmacTable.SetCell(row, 1, tview.NewTableCell(fmt.Sprintf("%.2f", value))) row++ } } } func main() { app := tview.NewApplication() // Create pages pages := tview.NewPages() // System page sysPage := createSystemPage() pages.AddPage("system", sysPage, true, true) // hmac-file-server page hmacPage := createHmacPage() pages.AddPage("hmac", hmacPage, true, false) // Add key binding to switch views app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyRune { switch event.Rune() { case 'q', 'Q': app.Stop() return nil case 's', 'S': // Switch to system page pages.SwitchToPage("system") return nil case 'h', 'H': // Switch to hmac-file-server page pages.SwitchToPage("hmac") return nil } } return event }) // Start the UI update loop in a separate goroutine go updateUI(app, pages, sysPage, hmacPage) // Set the root and run the application if err := app.SetRoot(pages, true).EnableMouse(true).Run(); err != nil { log.Fatalf("Error running application: %v", err) } } // Function to create the system page func createSystemPage() tview.Primitive { // Create system data table sysTable := tview.NewTable().SetBorders(false) sysTable.SetTitle(" [::b]System Data ").SetBorder(true) // Create Prometheus metrics table metricsTable := tview.NewTable().SetBorders(false) metricsTable.SetTitle(" [::b]Prometheus Metrics ").SetBorder(true) // Create process list table processTable := tview.NewTable().SetBorders(false) processTable.SetTitle(" [::b]Process List ").SetBorder(true) // Create a flex layout to hold the tables sysFlex := tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(sysTable, 7, 0, false). // Fixed height for system data AddItem(metricsTable, 0, 1, false). // Proportional height for metrics AddItem(processTable, 0, 2, false) // Proportional height for process list return sysFlex } // Function to create the hmac-file-server page func createHmacPage() tview.Primitive { hmacTable := tview.NewTable().SetBorders(false) hmacTable.SetTitle(" [::b]hmac-file-server Details ").SetBorder(true) hmacFlex := tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(hmacTable, 0, 1, false) return hmacFlex }