449 lines
12 KiB
Go
449 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"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"
|
|
)
|
|
|
|
const prometheusURL = "http://localhost:9090/metrics"
|
|
|
|
// 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-file-server 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
|
|
}
|