release: hmac-file-server 3.2
This commit is contained in:
1050
cmd/monitor/monitor.go
Normal file
1050
cmd/monitor/monitor.go
Normal file
@@ -0,0 +1,1050 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"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
|
||||
configFilePath string // Pfad der gefundenen Konfiguration
|
||||
logFilePath string // Pfad der Logdatei aus der Konfiguration
|
||||
metricsEnabled bool // Neue Variable für die Aktivierung von Metriken
|
||||
bindIP string // Neue Variable für die gebundene IP-Adresse
|
||||
)
|
||||
|
||||
func init() {
|
||||
configPaths := []string{
|
||||
"/etc/hmac-file-server/config.toml",
|
||||
"../config.toml",
|
||||
"./config.toml",
|
||||
}
|
||||
|
||||
var config *toml.Tree
|
||||
var err error
|
||||
|
||||
// Lade die config.toml aus den definierten Pfaden
|
||||
for _, path := range configPaths {
|
||||
config, err = toml.LoadFile(path)
|
||||
if err == nil {
|
||||
configFilePath = path
|
||||
log.Printf("Using config file: %s", configFilePath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading config file: %v\nPlease create a config.toml in one of the following locations:\n%v", err, configPaths)
|
||||
}
|
||||
|
||||
// Metricsport auslesen
|
||||
portValue := config.Get("server.metricsport")
|
||||
if portValue == nil {
|
||||
log.Println("Warning: 'server.metricsport' is missing in the configuration, using default port 9090")
|
||||
portValue = int64(9090)
|
||||
}
|
||||
|
||||
var port int64
|
||||
switch v := portValue.(type) {
|
||||
case int64:
|
||||
port = v
|
||||
case string:
|
||||
parsedPort, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing 'server.metricsport' as int64: %v", err)
|
||||
}
|
||||
port = parsedPort
|
||||
default:
|
||||
log.Fatalf("Error: 'server.metricsport' is not of type int64 or string, got %T", v)
|
||||
}
|
||||
|
||||
// Lesen von 'metricsenabled' aus der Konfiguration
|
||||
metricsEnabledValue := config.Get("server.metricsenabled")
|
||||
if metricsEnabledValue == nil {
|
||||
log.Println("Warning: 'server.metricsenabled' ist in der Konfiguration nicht gesetzt. Standardmäßig deaktiviert.")
|
||||
metricsEnabled = false
|
||||
} else {
|
||||
var ok bool
|
||||
metricsEnabled, ok = metricsEnabledValue.(bool)
|
||||
if !ok {
|
||||
log.Fatalf("Konfigurationsfehler: 'server.metricsenabled' sollte ein boolescher Wert sein, aber %T wurde gefunden.", metricsEnabledValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Lesen von 'bind_ip' aus der Konfiguration
|
||||
bindIPValue := config.Get("server.bind_ip")
|
||||
if bindIPValue == nil {
|
||||
log.Println("Warning: 'server.bind_ip' ist in der Konfiguration nicht gesetzt. Standardmäßig auf 'localhost' gesetzt.")
|
||||
bindIP = "localhost"
|
||||
} else {
|
||||
var ok bool
|
||||
bindIP, ok = bindIPValue.(string)
|
||||
if !ok {
|
||||
log.Fatalf("Konfigurationsfehler: 'server.bind_ip' sollte ein String sein, aber %T wurde gefunden.", bindIPValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Konstruktion der prometheusURL basierend auf 'bind_ip' und 'metricsport'
|
||||
prometheusURL = fmt.Sprintf("http://%s:%d/metrics", bindIP, port)
|
||||
log.Printf("Metrics URL gesetzt auf: %s", prometheusURL)
|
||||
|
||||
// Log-Datei auslesen über server.logfile
|
||||
logFileValue := config.Get("server.logfile")
|
||||
if logFileValue == nil {
|
||||
log.Println("Warning: 'server.logfile' is missing, using default '/var/log/hmac-file-server.log'")
|
||||
logFilePath = "/var/log/hmac-file-server.log"
|
||||
} else {
|
||||
lf, ok := logFileValue.(string)
|
||||
if !ok {
|
||||
log.Fatalf("Error: 'server.logfile' is not of type string, got %T", logFileValue)
|
||||
}
|
||||
logFilePath = lf
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
Uptime string // Neues Feld für die Uptime
|
||||
Status string // Neues Feld für den Status
|
||||
ErrorCount int // Neues Feld für die Anzahl der Fehler
|
||||
TotalRequests int64 // Neues Feld für die Gesamtanzahl der Anfragen
|
||||
ActiveConnections int // Neues Feld für aktive Verbindungen
|
||||
AverageResponseTime float64 // Neues Feld für die durchschnittliche Antwortzeit in Millisekunden
|
||||
}
|
||||
|
||||
// Optimized metrics fetching with timeout and connection reuse
|
||||
func fetchMetrics() (map[string]float64, error) {
|
||||
// Create HTTP client with timeout and connection reuse
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
DisableCompression: true,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Get(prometheusURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch metrics: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Limit response body size to prevent memory issues
|
||||
limitedReader := io.LimitReader(resp.Body, 1024*1024) // 1MB limit
|
||||
|
||||
parser := &expfmt.TextParser{}
|
||||
metricFamilies, err := parser.TextToMetricFamilies(limitedReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse metrics: %w", err)
|
||||
}
|
||||
|
||||
metrics := make(map[string]float64)
|
||||
|
||||
// More selective metric filtering to reduce processing
|
||||
relevantPrefixes := []string{
|
||||
"hmac_file_server_",
|
||||
"memory_usage_bytes",
|
||||
"cpu_usage_percent",
|
||||
"active_connections_total",
|
||||
"goroutines_count",
|
||||
"total_requests",
|
||||
"average_response_time_ms",
|
||||
}
|
||||
|
||||
for name, mf := range metricFamilies {
|
||||
// Quick prefix check to skip irrelevant metrics
|
||||
relevant := false
|
||||
for _, prefix := range relevantPrefixes {
|
||||
if strings.HasPrefix(name, prefix) || name == prefix {
|
||||
relevant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !relevant {
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Simplified label handling
|
||||
if len(m.GetLabel()) > 0 {
|
||||
labels := make([]string, 0, len(m.GetLabel()))
|
||||
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
|
||||
}
|
||||
|
||||
// Optimized process list fetching with better resource management
|
||||
func fetchProcessList() ([]ProcessInfo, error) {
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch processes: %w", err)
|
||||
}
|
||||
|
||||
// Pre-allocate slice with reasonable capacity
|
||||
processList := make([]ProcessInfo, 0, len(processes))
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Limit concurrent goroutines to prevent resource exhaustion
|
||||
sem := make(chan struct{}, 5) // Reduced from 10 to 5
|
||||
timeout := time.After(10 * time.Second) // Add timeout
|
||||
|
||||
// Process only a subset of processes to reduce load
|
||||
maxProcesses := 200
|
||||
if len(processes) > maxProcesses {
|
||||
processes = processes[:maxProcesses]
|
||||
}
|
||||
|
||||
for _, p := range processes {
|
||||
select {
|
||||
case <-timeout:
|
||||
log.Printf("Process list fetch timeout, returning partial results")
|
||||
return processList, nil
|
||||
default:
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
sem <- struct{}{} // Enter semaphore
|
||||
|
||||
go func(p *process.Process) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
<-sem // Exit semaphore
|
||||
// Recover from any panics in process info fetching
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Process info fetch panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Set shorter timeout for individual process operations
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use context for process operations where possible
|
||||
cpuPercent, err := p.CPUPercentWithContext(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
memPercent, err := p.MemoryPercentWithContext(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
name, err := p.NameWithContext(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if CPU and memory usage are both very low to reduce noise
|
||||
if cpuPercent < 0.1 && memPercent < 0.1 {
|
||||
return
|
||||
}
|
||||
|
||||
// Limit command line length to prevent memory bloat
|
||||
cmdline, err := p.CmdlineWithContext(ctx)
|
||||
if err != nil {
|
||||
cmdline = ""
|
||||
}
|
||||
if len(cmdline) > 100 {
|
||||
cmdline = cmdline[:100] + "..."
|
||||
}
|
||||
|
||||
info := ProcessInfo{
|
||||
PID: p.Pid,
|
||||
Name: name,
|
||||
CPUPercent: cpuPercent,
|
||||
MemPercent: memPercent,
|
||||
CommandLine: cmdline,
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
processList = append(processList, info)
|
||||
mu.Unlock()
|
||||
}(p)
|
||||
}
|
||||
|
||||
// Wait with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// All goroutines completed
|
||||
case <-time.After(15 * time.Second):
|
||||
log.Printf("Process list fetch timeout after 15 seconds, returning partial results")
|
||||
}
|
||||
|
||||
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 = ""
|
||||
}
|
||||
|
||||
createTime, err := p.CreateTime()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get process start time: %w", err)
|
||||
}
|
||||
uptime := time.Since(time.Unix(0, createTime*int64(time.Millisecond)))
|
||||
|
||||
status := "Running" // Standardstatus
|
||||
|
||||
// Überprüfung, ob der Prozess aktiv ist
|
||||
isRunning, err := p.IsRunning()
|
||||
if err != nil || !isRunning {
|
||||
status = "Stopped"
|
||||
}
|
||||
|
||||
errorCount, err := countHmacErrors()
|
||||
if err != nil {
|
||||
errorCount = 0
|
||||
}
|
||||
|
||||
metrics, err := fetchMetrics()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch metrics: %w", err)
|
||||
}
|
||||
|
||||
totalRequests, ok := metrics["total_requests"]
|
||||
if !ok {
|
||||
totalRequests = 0
|
||||
}
|
||||
|
||||
activeConnections, ok := metrics["active_connections_total"]
|
||||
if !ok {
|
||||
activeConnections = 0
|
||||
}
|
||||
|
||||
averageResponseTime, ok := metrics["average_response_time_ms"]
|
||||
if !ok {
|
||||
averageResponseTime = 0.0
|
||||
}
|
||||
|
||||
return &ProcessInfo{
|
||||
PID: p.Pid,
|
||||
Name: name,
|
||||
CPUPercent: cpuPercent,
|
||||
MemPercent: memPercent,
|
||||
CommandLine: cmdline,
|
||||
Uptime: uptime.String(),
|
||||
Status: status,
|
||||
ErrorCount: errorCount,
|
||||
TotalRequests: int64(totalRequests),
|
||||
ActiveConnections: int(activeConnections),
|
||||
AverageResponseTime: averageResponseTime,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("hmac-file-server process not found")
|
||||
}
|
||||
|
||||
// Optimized error counting with caching and limits
|
||||
var (
|
||||
errorCountCache int
|
||||
errorCountCacheTime time.Time
|
||||
errorCountMutex sync.RWMutex
|
||||
)
|
||||
|
||||
func countHmacErrors() (int, error) {
|
||||
// Use cached value if recent (within 30 seconds)
|
||||
errorCountMutex.RLock()
|
||||
if time.Since(errorCountCacheTime) < 30*time.Second {
|
||||
count := errorCountCache
|
||||
errorCountMutex.RUnlock()
|
||||
return count, nil
|
||||
}
|
||||
errorCountMutex.RUnlock()
|
||||
|
||||
// Use the configured log file path
|
||||
file, err := os.Open(logFilePath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Get file size to limit reading for very large files
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Limit to last 1MB for large log files
|
||||
var startPos int64 = 0
|
||||
if stat.Size() > 1024*1024 {
|
||||
startPos = stat.Size() - 1024*1024
|
||||
file.Seek(startPos, io.SeekStart)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
errorCount := 0
|
||||
lineCount := 0
|
||||
maxLines := 1000 // Limit lines scanned
|
||||
|
||||
for scanner.Scan() && lineCount < maxLines {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, "level=error") {
|
||||
errorCount++
|
||||
}
|
||||
lineCount++
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Update cache
|
||||
errorCountMutex.Lock()
|
||||
errorCountCache = errorCount
|
||||
errorCountCacheTime = time.Now()
|
||||
errorCountMutex.Unlock()
|
||||
|
||||
return errorCount, nil
|
||||
}
|
||||
|
||||
// Optimized data structure for caching
|
||||
type cachedData struct {
|
||||
systemData systemData
|
||||
metrics map[string]float64
|
||||
processes []ProcessInfo
|
||||
hmacInfo *ProcessInfo
|
||||
lastUpdate time.Time
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type systemData struct {
|
||||
memUsage float64
|
||||
cpuUsage float64
|
||||
cores int
|
||||
}
|
||||
|
||||
var cache = &cachedData{}
|
||||
|
||||
// Optimized updateUI with reduced frequency and better resource management
|
||||
func updateUI(ctx context.Context, app *tview.Application, pages *tview.Pages, sysPage, hmacPage tview.Primitive) {
|
||||
// Reduce update frequency significantly
|
||||
fastTicker := time.NewTicker(5 * time.Second) // UI updates
|
||||
slowTicker := time.NewTicker(15 * time.Second) // Process list updates
|
||||
defer fastTicker.Stop()
|
||||
defer slowTicker.Stop()
|
||||
|
||||
// Worker pool to limit concurrent operations
|
||||
workerPool := make(chan struct{}, 3) // Max 3 concurrent operations
|
||||
|
||||
// Single goroutine for data collection
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Data collection goroutine recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-fastTicker.C:
|
||||
// Only update system data and metrics (lightweight operations)
|
||||
select {
|
||||
case workerPool <- struct{}{}:
|
||||
go func() {
|
||||
defer func() { <-workerPool }()
|
||||
updateSystemAndMetrics()
|
||||
}()
|
||||
default:
|
||||
// Skip if worker pool is full
|
||||
}
|
||||
case <-slowTicker.C:
|
||||
// Update process list less frequently (expensive operation)
|
||||
select {
|
||||
case workerPool <- struct{}{}:
|
||||
go func() {
|
||||
defer func() { <-workerPool }()
|
||||
updateProcessData()
|
||||
}()
|
||||
default:
|
||||
// Skip if worker pool is full
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// UI update loop
|
||||
uiTicker := time.NewTicker(2 * time.Second)
|
||||
defer uiTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-uiTicker.C:
|
||||
app.QueueUpdateDraw(func() {
|
||||
updateUIComponents(pages, sysPage, hmacPage)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separate function to update system data and metrics
|
||||
func updateSystemAndMetrics() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("updateSystemAndMetrics recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get system data
|
||||
memUsage, cpuUsage, cores, err := fetchSystemData()
|
||||
if err != nil {
|
||||
log.Printf("Error fetching system data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get metrics if enabled
|
||||
var metrics map[string]float64
|
||||
if metricsEnabled {
|
||||
metrics, err = fetchMetrics()
|
||||
if err != nil {
|
||||
log.Printf("Error fetching metrics: %v", err)
|
||||
metrics = make(map[string]float64) // Use empty map on error
|
||||
}
|
||||
}
|
||||
|
||||
// Update cache
|
||||
cache.mu.Lock()
|
||||
cache.systemData = systemData{memUsage, cpuUsage, cores}
|
||||
cache.metrics = metrics
|
||||
cache.lastUpdate = time.Now()
|
||||
cache.mu.Unlock()
|
||||
}
|
||||
|
||||
// Separate function to update process data (expensive operation)
|
||||
func updateProcessData() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("updateProcessData recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get process list
|
||||
processes, err := fetchProcessList()
|
||||
if err != nil {
|
||||
log.Printf("Error fetching process list: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get HMAC info
|
||||
hmacInfo, err := fetchHmacFileServerInfo()
|
||||
if err != nil {
|
||||
log.Printf("Error fetching HMAC info: %v", err)
|
||||
}
|
||||
|
||||
// Update cache
|
||||
cache.mu.Lock()
|
||||
cache.processes = processes
|
||||
cache.hmacInfo = hmacInfo
|
||||
cache.mu.Unlock()
|
||||
}
|
||||
|
||||
// Update UI components with cached data
|
||||
func updateUIComponents(pages *tview.Pages, sysPage, hmacPage tview.Primitive) {
|
||||
currentPage, _ := pages.GetFrontPage()
|
||||
|
||||
cache.mu.RLock()
|
||||
defer cache.mu.RUnlock()
|
||||
|
||||
switch currentPage {
|
||||
case "system":
|
||||
sysFlex := sysPage.(*tview.Flex)
|
||||
|
||||
// Update system table
|
||||
sysTable := sysFlex.GetItem(0).(*tview.Table)
|
||||
updateSystemTable(sysTable, cache.systemData.memUsage, cache.systemData.cpuUsage, cache.systemData.cores)
|
||||
|
||||
// Update metrics table
|
||||
if metricsEnabled && len(cache.metrics) > 0 {
|
||||
metricsTable := sysFlex.GetItem(1).(*tview.Table)
|
||||
updateMetricsTable(metricsTable, cache.metrics)
|
||||
}
|
||||
|
||||
// Update process table
|
||||
if len(cache.processes) > 0 {
|
||||
processTable := sysFlex.GetItem(2).(*tview.Table)
|
||||
updateProcessTable(processTable, cache.processes)
|
||||
}
|
||||
|
||||
case "hmac":
|
||||
if cache.hmacInfo != nil {
|
||||
hmacFlex := hmacPage.(*tview.Flex)
|
||||
hmacTable := hmacFlex.GetItem(0).(*tview.Table)
|
||||
updateHmacTable(hmacTable, cache.hmacInfo, cache.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))
|
||||
|
||||
hmacTable.SetCell(5, 0, tview.NewTableCell("Uptime"))
|
||||
hmacTable.SetCell(5, 1, tview.NewTableCell(hmacInfo.Uptime)) // Neue Zeile für Uptime
|
||||
|
||||
hmacTable.SetCell(6, 0, tview.NewTableCell("Status"))
|
||||
hmacTable.SetCell(6, 1, tview.NewTableCell(hmacInfo.Status)) // Neue Zeile für Status
|
||||
|
||||
hmacTable.SetCell(7, 0, tview.NewTableCell("Error Count"))
|
||||
hmacTable.SetCell(7, 1, tview.NewTableCell(fmt.Sprintf("%d", hmacInfo.ErrorCount))) // Neue Zeile für Error Count
|
||||
|
||||
hmacTable.SetCell(8, 0, tview.NewTableCell("Total Requests"))
|
||||
hmacTable.SetCell(8, 1, tview.NewTableCell(fmt.Sprintf("%d", hmacInfo.TotalRequests))) // Neue Zeile für Total Requests
|
||||
|
||||
hmacTable.SetCell(9, 0, tview.NewTableCell("Active Connections"))
|
||||
hmacTable.SetCell(9, 1, tview.NewTableCell(fmt.Sprintf("%d", hmacInfo.ActiveConnections))) // Neue Zeile für Active Connections
|
||||
|
||||
hmacTable.SetCell(10, 0, tview.NewTableCell("Avg. Response Time (ms)"))
|
||||
hmacTable.SetCell(10, 1, tview.NewTableCell(fmt.Sprintf("%.2f", hmacInfo.AverageResponseTime))) // Neue Zeile für Average Response Time
|
||||
|
||||
// Metrics related to hmac-file-server
|
||||
row := 12
|
||||
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 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).
|
||||
AddItem(metricsTable, 0, 1, false).
|
||||
AddItem(processTable, 0, 2, false)
|
||||
|
||||
return sysFlex
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func createLogsPage(ctx context.Context, app *tview.Application, logFilePath string) tview.Primitive {
|
||||
logsTextView := tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetRegions(true).
|
||||
SetWordWrap(true)
|
||||
logsTextView.SetTitle(" [::b]Logs ").SetBorder(true)
|
||||
|
||||
const numLines = 50 // Reduced from 100 to 50 lines
|
||||
|
||||
// Cache for log content to avoid reading file too frequently
|
||||
var lastLogUpdate time.Time
|
||||
var logMutex sync.RWMutex
|
||||
|
||||
// Read logs less frequently and only when on logs page
|
||||
go func() {
|
||||
ticker := time.NewTicker(5 * time.Second) // Increased from 2 to 5 seconds
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Only update if we haven't updated recently
|
||||
logMutex.RLock()
|
||||
timeSinceUpdate := time.Since(lastLogUpdate)
|
||||
logMutex.RUnlock()
|
||||
|
||||
if timeSinceUpdate < 4*time.Second {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := readLastNLines(logFilePath, numLines)
|
||||
if err != nil {
|
||||
app.QueueUpdateDraw(func() {
|
||||
logsTextView.SetText(fmt.Sprintf("[red]Error reading log file: %v[white]", err))
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Process the log content with color coding
|
||||
lines := strings.Split(content, "\n")
|
||||
var coloredLines []string
|
||||
|
||||
// Limit the number of lines processed
|
||||
maxLines := min(len(lines), numLines)
|
||||
coloredLines = make([]string, 0, maxLines)
|
||||
|
||||
for i := len(lines) - maxLines; i < len(lines); i++ {
|
||||
if i < 0 {
|
||||
continue
|
||||
}
|
||||
line := lines[i]
|
||||
if strings.Contains(line, "level=info") {
|
||||
coloredLines = append(coloredLines, "[green]"+line+"[white]")
|
||||
} else if strings.Contains(line, "level=warn") {
|
||||
coloredLines = append(coloredLines, "[yellow]"+line+"[white]")
|
||||
} else if strings.Contains(line, "level=error") {
|
||||
coloredLines = append(coloredLines, "[red]"+line+"[white]")
|
||||
} else {
|
||||
coloredLines = append(coloredLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
logContent := strings.Join(coloredLines, "\n")
|
||||
|
||||
// Update cache
|
||||
logMutex.Lock()
|
||||
lastLogUpdate = time.Now()
|
||||
logMutex.Unlock()
|
||||
|
||||
app.QueueUpdateDraw(func() {
|
||||
logsTextView.SetText(logContent)
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return logsTextView
|
||||
}
|
||||
|
||||
// Helper function for min
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Optimized readLastNLines to handle large files efficiently
|
||||
func readLastNLines(filePath string, n int) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
const bufferSize = 1024
|
||||
buffer := make([]byte, bufferSize)
|
||||
var content []byte
|
||||
var fileSize int64
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fileSize = fileInfo.Size()
|
||||
|
||||
var offset int64 = 0
|
||||
for {
|
||||
if fileSize-offset < bufferSize {
|
||||
offset = fileSize
|
||||
} else {
|
||||
offset += bufferSize
|
||||
}
|
||||
|
||||
_, err := file.Seek(-offset, io.SeekEnd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bytesRead, err := file.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
|
||||
content = append(buffer[:bytesRead], content...)
|
||||
|
||||
if bytesRead < bufferSize || len(strings.Split(string(content), "\n")) > n+1 {
|
||||
break
|
||||
}
|
||||
|
||||
if offset >= fileSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
if len(lines) > n {
|
||||
lines = lines[len(lines)-n:]
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := tview.NewApplication()
|
||||
|
||||
// Create a cancellable context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// 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)
|
||||
|
||||
// Logs page mit dem gelesenen logFilePath
|
||||
logsPage := createLogsPage(ctx, app, logFilePath)
|
||||
pages.AddPage("logs", logsPage, true, false)
|
||||
|
||||
// Add key binding to switch views and handle exit
|
||||
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyRune {
|
||||
switch event.Rune() {
|
||||
case 'q', 'Q':
|
||||
cancel()
|
||||
app.Stop()
|
||||
return nil
|
||||
case 's', 'S':
|
||||
// Switch to system page
|
||||
pages.SwitchToPage("system")
|
||||
case 'h', 'H':
|
||||
// Switch to hmac-file-server page
|
||||
pages.SwitchToPage("hmac")
|
||||
case 'l', 'L':
|
||||
// Switch to logs page
|
||||
pages.SwitchToPage("logs")
|
||||
}
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
// Start the UI update loop in a separate goroutine
|
||||
go updateUI(ctx, 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)
|
||||
log.Fatalf("Error running application: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user