...
This commit is contained in:
		
							
								
								
									
										175
									
								
								pkg/system/stats/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								pkg/system/stats/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | ||||
| # System Stats Package | ||||
|  | ||||
| The `stats` package provides a comprehensive solution for collecting, caching, and retrieving system statistics in Go applications. It uses Redis for caching to minimize system resource usage when frequently accessing system metrics. | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| This package offers a thread-safe, configurable system for monitoring: | ||||
| - CPU usage and information | ||||
| - Memory utilization | ||||
| - Disk space and usage | ||||
| - Process statistics | ||||
| - Network speed and throughput | ||||
| - Hardware information | ||||
|  | ||||
| The `StatsManager` provides a central interface for accessing all system statistics with built-in caching, background updates, and configurable expiration times. | ||||
|  | ||||
| ## Key Components | ||||
|  | ||||
| ### StatsManager | ||||
|  | ||||
| The core component that manages all statistics collection with Redis-based caching: | ||||
|  | ||||
| - Thread-safe operations with mutex protection | ||||
| - Background worker for asynchronous updates | ||||
| - Configurable expiration times for different stat types | ||||
| - Debug mode for direct fetching without caching | ||||
| - Automatic cache initialization and updates | ||||
|  | ||||
| ### Statistics Types | ||||
|  | ||||
| The package collects various system statistics: | ||||
|  | ||||
| 1. **System Information** (`system`) | ||||
|    - CPU details (cores, model, usage percentage) | ||||
|    - Memory information (total, used, free, usage percentage) | ||||
|    - Network speed (upload and download) | ||||
|  | ||||
| 2. **Disk Statistics** (`disk`) | ||||
|    - Information for all mounted partitions | ||||
|    - Total, used, and free space | ||||
|    - Usage percentages | ||||
|  | ||||
| 3. **Process Statistics** (`process`) | ||||
|    - List of running processes | ||||
|    - CPU and memory usage per process | ||||
|    - Process status and creation time | ||||
|    - Command line information | ||||
|  | ||||
| 4. **Network Speed** (`network`) | ||||
|    - Current upload and download speeds | ||||
|    - Formatted with appropriate units (Mbps/Kbps) | ||||
|  | ||||
| 5. **Hardware Statistics** (`hardware`) | ||||
|    - Combined system, disk, and network information | ||||
|    - Formatted for easy display or JSON responses | ||||
|  | ||||
| ## Configuration | ||||
|  | ||||
| The `Config` struct allows customization of: | ||||
|  | ||||
| - Redis connection settings (address, password, database) | ||||
| - Expiration times for different types of statistics | ||||
| - Debug mode toggle | ||||
| - Default timeout for waiting for stats | ||||
| - Maximum queue size for update requests | ||||
|  | ||||
| Default configuration is provided through the `DefaultConfig()` function. | ||||
|  | ||||
| ## Usage Examples | ||||
|  | ||||
| ### Basic Usage | ||||
|  | ||||
| ```go | ||||
| // Create a stats manager with default settings | ||||
| manager, err := stats.NewStatsManagerWithDefaults() | ||||
| if err != nil { | ||||
|     log.Fatalf("Error creating stats manager: %v", err) | ||||
| } | ||||
| defer manager.Close() | ||||
|  | ||||
| // Get system information | ||||
| sysInfo, err := manager.GetSystemInfo() | ||||
| if err != nil { | ||||
|     log.Printf("Error getting system info: %v", err) | ||||
| } else { | ||||
|     fmt.Printf("CPU Cores: %d\n", sysInfo.CPU.Cores) | ||||
|     fmt.Printf("Memory Used: %.1f GB (%.1f%%)\n",  | ||||
|         sysInfo.Memory.Used, sysInfo.Memory.UsedPercent) | ||||
| } | ||||
|  | ||||
| // Get disk statistics | ||||
| diskStats, err := manager.GetDiskStats() | ||||
| if err != nil { | ||||
|     log.Printf("Error getting disk stats: %v", err) | ||||
| } else { | ||||
|     for _, disk := range diskStats.Disks { | ||||
|         fmt.Printf("%s: %.1f GB total, %.1f GB free (%.1f%% used)\n",  | ||||
|             disk.Path, disk.Total, disk.Free, disk.UsedPercent) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Get top processes by CPU usage | ||||
| processes, err := manager.GetTopProcesses(5) | ||||
| if err != nil { | ||||
|     log.Printf("Error getting top processes: %v", err) | ||||
| } else { | ||||
|     for i, proc := range processes { | ||||
|         fmt.Printf("%d. %s (PID: %d, CPU: %.1f%%, Memory: %.1f MB)\n",  | ||||
|             i+1, proc.Name, proc.PID, proc.CPUPercent, proc.MemoryMB) | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Custom Configuration | ||||
|  | ||||
| ```go | ||||
| // Create a custom configuration | ||||
| config := &stats.Config{ | ||||
|     RedisAddr:     "localhost:6379", | ||||
|     RedisPassword: "", | ||||
|     RedisDB:       0, | ||||
|     Debug:         false, | ||||
|     QueueSize:     100, | ||||
|     DefaultTimeout: 5 * time.Second, | ||||
|     ExpirationTimes: map[string]time.Duration{ | ||||
|         "system":   60 * time.Second,  // System info expires after 60 seconds | ||||
|         "disk":     300 * time.Second, // Disk info expires after 5 minutes | ||||
|         "process":  30 * time.Second,  // Process info expires after 30 seconds | ||||
|         "network":  30 * time.Second,  // Network info expires after 30 seconds | ||||
|         "hardware": 120 * time.Second, // Hardware stats expire after 2 minutes | ||||
|     }, | ||||
| } | ||||
|  | ||||
| manager, err := stats.NewStatsManager(config) | ||||
| if err != nil { | ||||
|     log.Fatalf("Error creating stats manager: %v", err) | ||||
| } | ||||
| defer manager.Close() | ||||
| ``` | ||||
|  | ||||
| ### Force Updates and Cache Management | ||||
|  | ||||
| ```go | ||||
| // Force an immediate update of system stats | ||||
| err := manager.ForceUpdate("system") | ||||
| if err != nil { | ||||
|     log.Printf("Error forcing update: %v", err) | ||||
| } | ||||
|  | ||||
| // Clear the cache for a specific stats type | ||||
| err = manager.ClearCache("disk") | ||||
| if err != nil { | ||||
|     log.Printf("Error clearing cache: %v", err) | ||||
| } | ||||
|  | ||||
| // Clear the entire cache | ||||
| err = manager.ClearCache("") | ||||
| if err != nil { | ||||
|     log.Printf("Error clearing entire cache: %v", err) | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Debug Mode | ||||
|  | ||||
| ```go | ||||
| // Enable debug mode to bypass caching | ||||
| manager.Debug = true | ||||
|  | ||||
| // Get stats directly without using cache | ||||
| sysInfo, err := manager.GetSystemInfo() | ||||
|  | ||||
| // Disable debug mode to resume caching | ||||
| manager.Debug = false | ||||
| ``` | ||||
|  | ||||
							
								
								
									
										164
									
								
								pkg/system/stats/cmd/manager_example/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								pkg/system/stats/cmd/manager_example/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/system/stats" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	fmt.Println("Stats Manager Example") | ||||
| 	fmt.Println("====================") | ||||
|  | ||||
| 	// Create a new stats manager with Redis connection | ||||
| 	// Create a custom configuration | ||||
| 	config := &stats.Config{ | ||||
| 		RedisAddr:     "localhost:6379", | ||||
| 		RedisPassword: "", | ||||
| 		RedisDB:       0, | ||||
| 		Debug:         false, | ||||
| 		QueueSize:     100, | ||||
| 		DefaultTimeout: 5 * time.Second, | ||||
| 		ExpirationTimes: map[string]time.Duration{ | ||||
| 			"system":   60 * time.Second,  // System info expires after 60 seconds | ||||
| 			"disk":     300 * time.Second, // Disk info expires after 5 minutes | ||||
| 			"process":  60 * time.Second,  // Process info expires after 1 minute | ||||
| 			"network":  30 * time.Second,  // Network info expires after 30 seconds | ||||
| 			"hardware": 120 * time.Second, // Hardware stats expire after 2 minutes | ||||
| 		}, | ||||
| 	} | ||||
| 	 | ||||
| 	manager, err := stats.NewStatsManager(config) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error creating stats manager: %v\n", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer manager.Close() | ||||
|  | ||||
| 	// Set up signal handling for graceful shutdown | ||||
| 	sigChan := make(chan os.Signal, 1) | ||||
| 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | ||||
| 	go func() { | ||||
| 		<-sigChan | ||||
| 		fmt.Println("\nShutting down...") | ||||
| 		manager.Close() | ||||
| 		os.Exit(0) | ||||
| 	}() | ||||
|  | ||||
| 	// Example 1: Get system info | ||||
| 	fmt.Println("\n1. SYSTEM INFORMATION") | ||||
| 	fmt.Println("--------------------") | ||||
| 	sysInfo, err := manager.GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting system info: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Println("CPU Information:") | ||||
| 		fmt.Printf("  Cores: %d\n", sysInfo.CPU.Cores) | ||||
| 		fmt.Printf("  Model: %s\n", sysInfo.CPU.ModelName) | ||||
| 		fmt.Printf("  Usage: %.1f%%\n", sysInfo.CPU.UsagePercent) | ||||
| 		 | ||||
| 		fmt.Println("\nMemory Information:") | ||||
| 		fmt.Printf("  Total: %.1f GB\n", sysInfo.Memory.Total) | ||||
| 		fmt.Printf("  Used: %.1f GB (%.1f%%)\n", sysInfo.Memory.Used, sysInfo.Memory.UsedPercent) | ||||
| 		fmt.Printf("  Free: %.1f GB\n", sysInfo.Memory.Free) | ||||
| 		 | ||||
| 		fmt.Println("\nNetwork Information:") | ||||
| 		fmt.Printf("  Upload Speed: %s\n", sysInfo.Network.UploadSpeed) | ||||
| 		fmt.Printf("  Download Speed: %s\n", sysInfo.Network.DownloadSpeed) | ||||
| 	} | ||||
|  | ||||
| 	// Example 2: Get disk stats | ||||
| 	fmt.Println("\n2. DISK INFORMATION") | ||||
| 	fmt.Println("------------------") | ||||
| 	diskStats, err := manager.GetDiskStats() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting disk stats: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Printf("Found %d disks:\n", len(diskStats.Disks)) | ||||
| 		for _, disk := range diskStats.Disks { | ||||
| 			fmt.Printf("  %s: %.1f GB total, %.1f GB free (%.1f%% used)\n",  | ||||
| 				disk.Path, disk.Total, disk.Free, disk.UsedPercent) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Example 3: Get process stats | ||||
| 	fmt.Println("\n3. PROCESS INFORMATION") | ||||
| 	fmt.Println("---------------------") | ||||
| 	processStats, err := manager.GetProcessStats(5) // Get top 5 processes | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting process stats: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Printf("Total processes: %d (showing top %d)\n",  | ||||
| 			processStats.Total, len(processStats.Processes)) | ||||
| 		 | ||||
| 		fmt.Println("\nTop Processes by CPU Usage:") | ||||
| 		for i, proc := range processStats.Processes { | ||||
| 			fmt.Printf("  %d. PID %d: %s (CPU: %.1f%%, Memory: %.1f MB)\n",  | ||||
| 				i+1, proc.PID, proc.Name, proc.CPUPercent, proc.MemoryMB) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Example 4: Demonstrate caching by getting the same data multiple times | ||||
| 	fmt.Println("\n4. CACHING DEMONSTRATION") | ||||
| 	fmt.Println("----------------------") | ||||
| 	fmt.Println("Getting network speed multiple times (should use cache):") | ||||
| 	 | ||||
| 	for i := 0; i < 3; i++ { | ||||
| 		netSpeed := manager.GetNetworkSpeedResult() | ||||
| 		fmt.Printf("  Request %d: Upload: %s, Download: %s\n",  | ||||
| 			i+1, netSpeed.UploadSpeed, netSpeed.DownloadSpeed) | ||||
| 		time.Sleep(500 * time.Millisecond) | ||||
| 	} | ||||
|  | ||||
| 	// Example 5: Get hardware stats JSON | ||||
| 	fmt.Println("\n5. HARDWARE STATS JSON") | ||||
| 	fmt.Println("--------------------") | ||||
| 	hardwareJSON := manager.GetHardwareStatsJSON() | ||||
| 	prettyJSON, _ := json.MarshalIndent(hardwareJSON, "", "  ") | ||||
| 	fmt.Println(string(prettyJSON)) | ||||
|  | ||||
| 	// Example 6: Debug mode demonstration | ||||
| 	fmt.Println("\n6. DEBUG MODE DEMONSTRATION") | ||||
| 	fmt.Println("--------------------------") | ||||
| 	fmt.Println("Enabling debug mode (direct fetching without cache)...") | ||||
| 	manager.Debug = true | ||||
| 	 | ||||
| 	fmt.Println("Getting system info in debug mode:") | ||||
| 	debugSysInfo, err := manager.GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Printf("  CPU Usage: %.1f%%\n", debugSysInfo.CPU.UsagePercent) | ||||
| 		fmt.Printf("  Memory Used: %.1f GB (%.1f%%)\n",  | ||||
| 			debugSysInfo.Memory.Used, debugSysInfo.Memory.UsedPercent) | ||||
| 	} | ||||
| 	 | ||||
| 	// Reset debug mode | ||||
| 	manager.Debug = false | ||||
|  | ||||
| 	// Example 7: Modify expiration times | ||||
| 	fmt.Println("\n7. CUSTOM EXPIRATION TIMES") | ||||
| 	fmt.Println("------------------------") | ||||
| 	fmt.Println("Current expiration times:") | ||||
| 	for statsType, duration := range manager.Expiration { | ||||
| 		fmt.Printf("  %s: %v\n", statsType, duration) | ||||
| 	} | ||||
| 	 | ||||
| 	fmt.Println("\nChanging system stats expiration to 10 seconds...") | ||||
| 	manager.Expiration["system"] = 10 * time.Second | ||||
| 	 | ||||
| 	fmt.Println("Updated expiration times:") | ||||
| 	for statsType, duration := range manager.Expiration { | ||||
| 		fmt.Printf("  %s: %v\n", statsType, duration) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println("\nDemo complete. Press Ctrl+C to exit.") | ||||
| 	 | ||||
| 	// Keep the program running | ||||
| 	select {} | ||||
| } | ||||
							
								
								
									
										285
									
								
								pkg/system/stats/cmd/performance_test/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								pkg/system/stats/cmd/performance_test/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"runtime" | ||||
| 	"runtime/pprof" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/system/stats" | ||||
| 	"github.com/shirou/gopsutil/v3/cpu" | ||||
| 	"github.com/shirou/gopsutil/v3/process" | ||||
| ) | ||||
|  | ||||
| // TestResult stores the results of a single test run | ||||
| type TestResult struct { | ||||
| 	StartTime       time.Time | ||||
| 	EndTime         time.Time | ||||
| 	SystemInfoTime  time.Duration | ||||
| 	DiskStatsTime   time.Duration | ||||
| 	ProcessTime     time.Duration | ||||
| 	NetworkTime     time.Duration | ||||
| 	HardwareTime    time.Duration | ||||
| 	TotalTime       time.Duration | ||||
| 	UserCPU         float64 | ||||
| 	SystemCPU       float64 | ||||
| 	TotalCPU        float64 | ||||
| 	OverallCPU      float64 | ||||
| 	MemoryUsageMB   float32 | ||||
| 	NumGoroutines   int | ||||
| } | ||||
|  | ||||
| func main() { | ||||
| 	// Parse command line flags | ||||
| 	intervalPtr := flag.Int("interval", 5, "Interval between tests in seconds") | ||||
| 	sleepPtr := flag.Int("sleep", 0, "Sleep time between operations in milliseconds") | ||||
| 	cpuProfilePtr := flag.String("cpuprofile", "", "Write cpu profile to file") | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	// If CPU profiling is enabled, set it up | ||||
| 	if *cpuProfilePtr != "" { | ||||
| 		f, err := os.Create(*cpuProfilePtr) | ||||
| 		if err != nil { | ||||
| 			fmt.Printf("Error creating CPU profile: %v\n", err) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		defer f.Close() | ||||
| 		if err := pprof.StartCPUProfile(f); err != nil { | ||||
| 			fmt.Printf("Error starting CPU profile: %v\n", err) | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 		defer pprof.StopCPUProfile() | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println("StatsManager Performance Test") | ||||
| 	fmt.Println("============================") | ||||
| 	fmt.Printf("This test measures the performance of retrieving stats from Redis cache\n") | ||||
| 	fmt.Printf("It will run every %d seconds and print performance metrics\n", *intervalPtr) | ||||
| 	fmt.Printf("Sleep between operations: %d ms\n", *sleepPtr) | ||||
| 	fmt.Println("Press Ctrl+C to exit and view summary statistics") | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	// Create a new stats manager with Redis connection | ||||
| 	config := &stats.Config{ | ||||
| 		RedisAddr:     "localhost:6379", | ||||
| 		RedisPassword: "", | ||||
| 		RedisDB:       0, | ||||
| 		Debug:         false, | ||||
| 		QueueSize:     100, | ||||
| 		DefaultTimeout: 5 * time.Second, | ||||
| 		ExpirationTimes: map[string]time.Duration{ | ||||
| 			"system":   60 * time.Second,  // System info expires after 60 seconds | ||||
| 			"disk":     300 * time.Second, // Disk info expires after 5 minutes | ||||
| 			"process":  60 * time.Second,  // Process info expires after 1 minute | ||||
| 			"network":  30 * time.Second,  // Network info expires after 30 seconds | ||||
| 			"hardware": 120 * time.Second, // Hardware stats expire after 2 minutes | ||||
| 		}, | ||||
| 	} | ||||
| 	 | ||||
| 	manager, err := stats.NewStatsManager(config) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error creating stats manager: %v\n", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer manager.Close() | ||||
|  | ||||
| 	// Initialize the cache with initial values | ||||
| 	fmt.Println("Initializing cache with initial values...") | ||||
| 	_, _ = manager.GetSystemInfo() | ||||
| 	_, _ = manager.GetDiskStats() | ||||
| 	_, _ = manager.GetProcessStats(10) | ||||
| 	_ = manager.GetNetworkSpeedResult() | ||||
| 	_ = manager.GetHardwareStatsJSON() | ||||
| 	fmt.Println("Cache initialized. Starting performance test...") | ||||
| 	fmt.Println() | ||||
|  | ||||
| 	// Set up signal handling for graceful shutdown | ||||
| 	sigChan := make(chan os.Signal, 1) | ||||
| 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | ||||
| 	 | ||||
| 	// Create a ticker for running tests at the specified interval | ||||
| 	ticker := time.NewTicker(time.Duration(*intervalPtr) * time.Second) | ||||
| 	defer ticker.Stop() | ||||
| 	 | ||||
| 	// Store the sleep duration between operations | ||||
| 	sleepDuration := time.Duration(*sleepPtr) * time.Millisecond | ||||
|  | ||||
| 	// Get the current process for CPU and memory measurements | ||||
| 	currentProcess, err := process.NewProcess(int32(os.Getpid())) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting current process: %v\n", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	// Store test results | ||||
| 	var results []TestResult | ||||
| 	 | ||||
| 	// Print header | ||||
| 	fmt.Printf("%-20s %-20s %-12s %-12s %-12s %-12s %-12s %-12s %-12s %-12s %-12s %-12s %-12s\n", | ||||
| 		"Start Time", "End Time", "System(ms)", "Disk(ms)", "Process(ms)", "Network(ms)", "Hardware(ms)", "Total(ms)", "UserCPU(%)", "SysCPU(%)", "TotalCPU(%)", "Memory(MB)", "Goroutines") | ||||
| 	fmt.Println(strings.Repeat("-", 180)) | ||||
|  | ||||
| 	// Run the test until interrupted | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-ticker.C: | ||||
| 			// Run a test and record the results | ||||
| 			result := runTest(manager, currentProcess, sleepDuration) | ||||
| 			results = append(results, result) | ||||
| 			 | ||||
| 			// Print the result | ||||
| 			fmt.Printf("%-20s %-20s %-12.2f %-12.2f %-12.2f %-12.2f %-12.2f %-12.2f %-12.2f %-12.2f %-12.2f %-12.2f %-12d\n", | ||||
| 				result.StartTime.Format("15:04:05.000000"), | ||||
| 				result.EndTime.Format("15:04:05.000000"), | ||||
| 				float64(result.SystemInfoTime.Microseconds())/1000, | ||||
| 				float64(result.DiskStatsTime.Microseconds())/1000, | ||||
| 				float64(result.ProcessTime.Microseconds())/1000, | ||||
| 				float64(result.NetworkTime.Microseconds())/1000, | ||||
| 				float64(result.HardwareTime.Microseconds())/1000, | ||||
| 				float64(result.TotalTime.Microseconds())/1000, | ||||
| 				result.UserCPU, | ||||
| 				result.SystemCPU, | ||||
| 				result.TotalCPU, | ||||
| 				result.MemoryUsageMB, | ||||
| 				result.NumGoroutines) | ||||
| 			 | ||||
| 		case <-sigChan: | ||||
| 			// Calculate and print summary statistics | ||||
| 			fmt.Println("\nTest Summary:") | ||||
| 			fmt.Println(strings.Repeat("-", 50)) | ||||
| 			 | ||||
| 			var totalSystemTime, totalDiskTime, totalProcessTime, totalNetworkTime, totalHardwareTime, totalTime time.Duration | ||||
| 			var totalUserCPU, totalSystemCPU, totalCombinedCPU, totalOverallCPU float64 | ||||
| 			var totalMemory float32 | ||||
| 			 | ||||
| 			for _, r := range results { | ||||
| 				totalSystemTime += r.SystemInfoTime | ||||
| 				totalDiskTime += r.DiskStatsTime | ||||
| 				totalProcessTime += r.ProcessTime | ||||
| 				totalNetworkTime += r.NetworkTime | ||||
| 				totalHardwareTime += r.HardwareTime | ||||
| 				totalTime += r.TotalTime | ||||
| 				totalUserCPU += r.UserCPU | ||||
| 				totalSystemCPU += r.SystemCPU | ||||
| 				totalCombinedCPU += r.TotalCPU | ||||
| 				totalOverallCPU += r.OverallCPU | ||||
| 				totalMemory += r.MemoryUsageMB | ||||
| 			} | ||||
| 			 | ||||
| 			count := float64(len(results)) | ||||
| 			if count > 0 { | ||||
| 				fmt.Printf("Average System Info Time:  %.2f ms\n", float64(totalSystemTime.Microseconds())/(count*1000)) | ||||
| 				fmt.Printf("Average Disk Stats Time:   %.2f ms\n", float64(totalDiskTime.Microseconds())/(count*1000)) | ||||
| 				fmt.Printf("Average Process Time:      %.2f ms\n", float64(totalProcessTime.Microseconds())/(count*1000)) | ||||
| 				fmt.Printf("Average Network Time:      %.2f ms\n", float64(totalNetworkTime.Microseconds())/(count*1000)) | ||||
| 				fmt.Printf("Average Hardware Time:     %.2f ms\n", float64(totalHardwareTime.Microseconds())/(count*1000)) | ||||
| 				fmt.Printf("Average Total Time:        %.2f ms\n", float64(totalTime.Microseconds())/(count*1000)) | ||||
| 				fmt.Printf("Average User CPU:          %.2f%%\n", totalUserCPU/count) | ||||
| 				fmt.Printf("Average System CPU:        %.2f%%\n", totalSystemCPU/count) | ||||
| 				fmt.Printf("Average Process CPU:       %.2f%%\n", totalCombinedCPU/count) | ||||
| 				fmt.Printf("Average Overall CPU:       %.2f%%\n", totalOverallCPU/count) | ||||
| 				fmt.Printf("Average Memory Usage:      %.2f MB\n", float64(totalMemory)/count) | ||||
| 			} | ||||
| 			 | ||||
| 			fmt.Println("\nTest completed. Exiting...") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // runTest runs a single test iteration and returns the results | ||||
| func runTest(manager *stats.StatsManager, proc *process.Process, sleepBetweenOps time.Duration) TestResult { | ||||
| 	// Get initial CPU times for the process | ||||
| 	initialTimes, _ := proc.Times() | ||||
| 	 | ||||
| 	// Get initial overall CPU usage | ||||
| 	_, _ = cpu.Percent(0, false) // Discard initial reading, we'll only use the final reading | ||||
| 	 | ||||
| 	result := TestResult{ | ||||
| 		StartTime: time.Now(), | ||||
| 	} | ||||
| 	 | ||||
| 	// Measure total time | ||||
| 	totalStart := time.Now() | ||||
| 	 | ||||
| 	// Measure system info time | ||||
| 	start := time.Now() | ||||
| 	_, _ = manager.GetSystemInfo() | ||||
| 	result.SystemInfoTime = time.Since(start) | ||||
| 	 | ||||
| 	// Sleep between operations if configured | ||||
| 	if sleepBetweenOps > 0 { | ||||
| 		time.Sleep(sleepBetweenOps) | ||||
| 	} | ||||
| 	 | ||||
| 	// Measure disk stats time | ||||
| 	start = time.Now() | ||||
| 	_, _ = manager.GetDiskStats() | ||||
| 	result.DiskStatsTime = time.Since(start) | ||||
| 	 | ||||
| 	// Sleep between operations if configured | ||||
| 	if sleepBetweenOps > 0 { | ||||
| 		time.Sleep(sleepBetweenOps) | ||||
| 	} | ||||
| 	 | ||||
| 	// Measure process stats time | ||||
| 	start = time.Now() | ||||
| 	_, _ = manager.GetProcessStats(10) | ||||
| 	result.ProcessTime = time.Since(start) | ||||
| 	 | ||||
| 	// Sleep between operations if configured | ||||
| 	if sleepBetweenOps > 0 { | ||||
| 		time.Sleep(sleepBetweenOps) | ||||
| 	} | ||||
| 	 | ||||
| 	// Measure network speed time | ||||
| 	start = time.Now() | ||||
| 	_ = manager.GetNetworkSpeedResult() | ||||
| 	result.NetworkTime = time.Since(start) | ||||
| 	 | ||||
| 	// Sleep between operations if configured | ||||
| 	if sleepBetweenOps > 0 { | ||||
| 		time.Sleep(sleepBetweenOps) | ||||
| 	} | ||||
| 	 | ||||
| 	// Measure hardware stats time | ||||
| 	start = time.Now() | ||||
| 	_ = manager.GetHardwareStatsJSON() | ||||
| 	result.HardwareTime = time.Since(start) | ||||
| 	 | ||||
| 	// Record total time | ||||
| 	result.TotalTime = time.Since(totalStart) | ||||
| 	result.EndTime = time.Now() | ||||
| 	 | ||||
| 	// Get final CPU times for the process | ||||
| 	finalTimes, _ := proc.Times() | ||||
| 	 | ||||
| 	// Calculate CPU usage for this specific operation | ||||
| 	if initialTimes != nil && finalTimes != nil { | ||||
| 		result.UserCPU = (finalTimes.User - initialTimes.User) * 100 | ||||
| 		result.SystemCPU = (finalTimes.System - initialTimes.System) * 100 | ||||
| 		result.TotalCPU = result.UserCPU + result.SystemCPU | ||||
| 	} | ||||
| 	 | ||||
| 	// Get overall CPU usage | ||||
| 	finalOverallCPU, _ := cpu.Percent(0, false) | ||||
| 	if len(finalOverallCPU) > 0 { | ||||
| 		result.OverallCPU = finalOverallCPU[0] | ||||
| 	} | ||||
| 	 | ||||
| 	// Measure memory usage | ||||
| 	memInfo, _ := proc.MemoryInfo() | ||||
| 	if memInfo != nil { | ||||
| 		result.MemoryUsageMB = float32(memInfo.RSS) / (1024 * 1024) | ||||
| 	} | ||||
| 	 | ||||
| 	// Record number of goroutines | ||||
| 	result.NumGoroutines = runtime.NumGoroutine() | ||||
| 	 | ||||
| 	return result | ||||
| } | ||||
							
								
								
									
										67
									
								
								pkg/system/stats/cmd/redis_test/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								pkg/system/stats/cmd/redis_test/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/system/stats" | ||||
| 	"github.com/redis/go-redis/v9" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	fmt.Println("Redis Connection Test") | ||||
| 	fmt.Println("====================") | ||||
|  | ||||
| 	// Create a configuration for Redis | ||||
| 	config := &stats.Config{ | ||||
| 		RedisAddr:     "localhost:6379", | ||||
| 		RedisPassword: "", | ||||
| 		RedisDB:       0, | ||||
| 	} | ||||
|  | ||||
| 	// Create Redis client | ||||
| 	client := redis.NewClient(&redis.Options{ | ||||
| 		Addr:     config.RedisAddr, | ||||
| 		Password: config.RedisPassword, | ||||
| 		DB:       config.RedisDB, | ||||
| 	}) | ||||
|  | ||||
| 	// Test connection with timeout | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	// Try to ping Redis | ||||
| 	pong, err := client.Ping(ctx).Result() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error connecting to Redis: %v\n", err) | ||||
| 		fmt.Println("\nPlease ensure Redis is running with:") | ||||
| 		fmt.Println("  - docker run --name redis -p 6379:6379 -d redis") | ||||
| 		fmt.Println("  - or install Redis locally and start the service") | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("Successfully connected to Redis at %s\n", config.RedisAddr) | ||||
| 	fmt.Printf("Response: %s\n", pong) | ||||
|  | ||||
| 	// Test basic operations | ||||
| 	fmt.Println("\nTesting basic Redis operations...") | ||||
|  | ||||
| 	// Set a key | ||||
| 	err = client.Set(ctx, "test:key", "Hello from HeroLauncher!", 1*time.Minute).Err() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error setting key: %v\n", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	// Get the key | ||||
| 	val, err := client.Get(ctx, "test:key").Result() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting key: %v\n", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("Retrieved value: %s\n", val) | ||||
| 	fmt.Println("Redis connection test successful!") | ||||
| } | ||||
							
								
								
									
										195
									
								
								pkg/system/stats/cmd/stats_test/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								pkg/system/stats/cmd/stats_test/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/heroagent/pkg/system/stats" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	fmt.Println("System Stats Test Program") | ||||
| 	fmt.Println("========================") | ||||
|  | ||||
| 	// Create a new stats manager with Redis connection | ||||
| 	config := &stats.Config{ | ||||
| 		RedisAddr:     "localhost:6379", | ||||
| 		RedisPassword: "", | ||||
| 		RedisDB:       0, | ||||
| 		Debug:         false, | ||||
| 		QueueSize:     100, | ||||
| 		DefaultTimeout: 5 * time.Second, | ||||
| 		ExpirationTimes: map[string]time.Duration{ | ||||
| 			"system":   30 * time.Second,  // System info expires after 30 seconds | ||||
| 			"disk":     60 * time.Second,  // Disk info expires after 1 minute | ||||
| 			"process":  15 * time.Second,  // Process info expires after 15 seconds | ||||
| 			"network":  20 * time.Second,  // Network info expires after 20 seconds | ||||
| 			"hardware": 60 * time.Second,  // Hardware stats expire after 1 minute | ||||
| 		}, | ||||
| 	} | ||||
| 	 | ||||
| 	manager, err := stats.NewStatsManager(config) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error creating stats manager: %v\n", err) | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
| 	defer manager.Close() | ||||
|  | ||||
| 	// DISK INFORMATION | ||||
| 	fmt.Println("\n1. DISK INFORMATION") | ||||
| 	fmt.Println("------------------") | ||||
| 	 | ||||
| 	// Get all disk stats using the manager | ||||
| 	diskStats, err := manager.GetDiskStats() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting disk stats: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Printf("Found %d disks:\n", len(diskStats.Disks)) | ||||
| 		for _, disk := range diskStats.Disks { | ||||
| 			fmt.Printf("  %s: %.1f GB total, %.1f GB free (%.1f%% used)\n",  | ||||
| 				disk.Path, disk.Total, disk.Free, disk.UsedPercent) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Get root disk info using the manager | ||||
| 	rootDisk, err := manager.GetRootDiskInfo() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting root disk info: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Printf("\nRoot Disk: %.1f GB total, %.1f GB free (%.1f%% used)\n",  | ||||
| 			rootDisk.Total, rootDisk.Free, rootDisk.UsedPercent) | ||||
| 	} | ||||
|  | ||||
| 	// Get formatted disk info | ||||
| 	fmt.Printf("Formatted Disk Info: %s\n", manager.GetFormattedDiskInfo()) | ||||
|  | ||||
| 	// SYSTEM INFORMATION | ||||
| 	fmt.Println("\n2. SYSTEM INFORMATION") | ||||
| 	fmt.Println("--------------------") | ||||
| 	 | ||||
| 	// Get system info using the manager | ||||
| 	sysInfo, err := manager.GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting system info: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Println("CPU Information:") | ||||
| 		fmt.Printf("  Cores: %d\n", sysInfo.CPU.Cores) | ||||
| 		fmt.Printf("  Model: %s\n", sysInfo.CPU.ModelName) | ||||
| 		fmt.Printf("  Usage: %.1f%%\n", sysInfo.CPU.UsagePercent) | ||||
| 		 | ||||
| 		fmt.Println("\nMemory Information:") | ||||
| 		fmt.Printf("  Total: %.1f GB\n", sysInfo.Memory.Total) | ||||
| 		fmt.Printf("  Used: %.1f GB (%.1f%%)\n", sysInfo.Memory.Used, sysInfo.Memory.UsedPercent) | ||||
| 		fmt.Printf("  Free: %.1f GB\n", sysInfo.Memory.Free) | ||||
| 		 | ||||
| 		fmt.Println("\nNetwork Information:") | ||||
| 		fmt.Printf("  Upload Speed: %s\n", sysInfo.Network.UploadSpeed) | ||||
| 		fmt.Printf("  Download Speed: %s\n", sysInfo.Network.DownloadSpeed) | ||||
| 	} | ||||
|  | ||||
| 	// Get network speed using the manager | ||||
| 	fmt.Println("\nNetwork Speed Test:") | ||||
| 	netSpeed := manager.GetNetworkSpeedResult() | ||||
| 	fmt.Printf("  Upload: %s\n", netSpeed.UploadSpeed) | ||||
| 	fmt.Printf("  Download: %s\n", netSpeed.DownloadSpeed) | ||||
|  | ||||
| 	// PROCESS INFORMATION | ||||
| 	fmt.Println("\n3. PROCESS INFORMATION") | ||||
| 	fmt.Println("---------------------") | ||||
| 	 | ||||
| 	// Get process stats using the manager | ||||
| 	processStats, err := manager.GetProcessStats(5) // Get top 5 processes | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting process stats: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Printf("Total processes: %d (showing top %d)\n",  | ||||
| 			processStats.Total, len(processStats.Processes)) | ||||
| 		 | ||||
| 		fmt.Println("\nTop Processes by CPU Usage:") | ||||
| 		for i, proc := range processStats.Processes { | ||||
| 			fmt.Printf("  %d. PID %d: %s (CPU: %.1f%%, Memory: %.1f MB)\n",  | ||||
| 				i+1, proc.PID, proc.Name, proc.CPUPercent, proc.MemoryMB) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Get top processes using the manager | ||||
| 	fmt.Println("\nTop 3 Processes:") | ||||
| 	topProcs, err := manager.GetTopProcesses(3) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting top processes: %v\n", err) | ||||
| 	} else { | ||||
| 		for i, proc := range topProcs { | ||||
| 			fmt.Printf("  %d. %s (PID %d)\n", i+1, proc.Name, proc.PID) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// COMBINED STATS | ||||
| 	fmt.Println("\n4. COMBINED STATS FUNCTIONS") | ||||
| 	fmt.Println("--------------------------") | ||||
| 	 | ||||
| 	// Hardware stats using the manager | ||||
| 	fmt.Println("\nHardware Stats:") | ||||
| 	hardwareStats := manager.GetHardwareStats() | ||||
| 	for key, value := range hardwareStats { | ||||
| 		fmt.Printf("  %s: %v\n", key, value) | ||||
| 	} | ||||
|  | ||||
| 	// Hardware stats JSON using the manager | ||||
| 	fmt.Println("\nHardware Stats (JSON):") | ||||
| 	hardwareJSON := manager.GetHardwareStatsJSON() | ||||
| 	prettyJSON, _ := json.MarshalIndent(hardwareJSON, "", "  ") | ||||
| 	fmt.Println(string(prettyJSON)) | ||||
|  | ||||
| 	// Process stats JSON using the manager | ||||
| 	fmt.Println("\nProcess Stats (JSON):") | ||||
| 	processJSON := manager.GetProcessStatsJSON(3) // Top 3 processes | ||||
| 	prettyJSON, _ = json.MarshalIndent(processJSON, "", "  ") | ||||
| 	fmt.Println(string(prettyJSON)) | ||||
|  | ||||
| 	// Wait and measure network speed again | ||||
| 	fmt.Println("\nWaiting 2 seconds for another network speed measurement...") | ||||
| 	time.Sleep(2 * time.Second) | ||||
| 	 | ||||
| 	// Get updated network speed using the manager | ||||
| 	updatedNetSpeed := manager.GetNetworkSpeedResult() | ||||
| 	fmt.Println("\nUpdated Network Speed:") | ||||
| 	fmt.Printf("  Upload: %s\n", updatedNetSpeed.UploadSpeed) | ||||
| 	fmt.Printf("  Download: %s\n", updatedNetSpeed.DownloadSpeed) | ||||
| 	 | ||||
| 	// CACHE MANAGEMENT | ||||
| 	fmt.Println("\n5. CACHE MANAGEMENT") | ||||
| 	fmt.Println("------------------") | ||||
| 	 | ||||
| 	// Force update of system stats | ||||
| 	fmt.Println("\nForcing update of system stats...") | ||||
| 	err = manager.ForceUpdate("system") | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error forcing update: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Println("System stats updated successfully") | ||||
| 	} | ||||
| 	 | ||||
| 	// Get updated system info | ||||
| 	updatedSysInfo, err := manager.GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error getting updated system info: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Println("\nUpdated CPU Usage: " + fmt.Sprintf("%.1f%%", updatedSysInfo.CPU.UsagePercent)) | ||||
| 	} | ||||
| 	 | ||||
| 	// Clear cache for disk stats | ||||
| 	fmt.Println("\nClearing cache for disk stats...") | ||||
| 	err = manager.ClearCache("disk") | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Error clearing cache: %v\n", err) | ||||
| 	} else { | ||||
| 		fmt.Println("Disk stats cache cleared successfully") | ||||
| 	} | ||||
| 	 | ||||
| 	// Toggle debug mode | ||||
| 	fmt.Println("\nToggling debug mode (direct fetching without cache)...") | ||||
| 	manager.Debug = !manager.Debug | ||||
| 	fmt.Printf("Debug mode is now: %v\n", manager.Debug) | ||||
| } | ||||
							
								
								
									
										44
									
								
								pkg/system/stats/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								pkg/system/stats/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| package stats | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // Config contains configuration options for the StatsManager | ||||
| type Config struct { | ||||
| 	// Redis connection settings | ||||
| 	RedisAddr     string | ||||
| 	RedisPassword string | ||||
| 	RedisDB       int | ||||
| 	 | ||||
| 	// Default expiration times for different types of stats (in seconds) | ||||
| 	ExpirationTimes map[string]time.Duration | ||||
| 	 | ||||
| 	// Debug mode - if true, requests are direct without caching | ||||
| 	Debug bool | ||||
| 	 | ||||
| 	// Default timeout for waiting for stats (in seconds) | ||||
| 	DefaultTimeout time.Duration | ||||
| 	 | ||||
| 	// Maximum queue size for update requests | ||||
| 	QueueSize int | ||||
| } | ||||
|  | ||||
| // DefaultConfig returns the default configuration for StatsManager | ||||
| func DefaultConfig() *Config { | ||||
| 	return &Config{ | ||||
| 		RedisAddr:     "localhost:6379", | ||||
| 		RedisPassword: "", | ||||
| 		RedisDB:       0, | ||||
| 		ExpirationTimes: map[string]time.Duration{ | ||||
| 			"system":   60 * time.Second,  // System info expires after 60 seconds | ||||
| 			"disk":     300 * time.Second, // Disk info expires after 5 minutes | ||||
| 			"process":  30 * time.Second,  // Process info expires after 30 seconds | ||||
| 			"network":  30 * time.Second,  // Network info expires after 30 seconds | ||||
| 			"hardware": 120 * time.Second, // Hardware stats expire after 2 minutes | ||||
| 		}, | ||||
| 		Debug:          false, | ||||
| 		DefaultTimeout: 60 * time.Second, // 1 minute default timeout | ||||
| 		QueueSize:      100, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										87
									
								
								pkg/system/stats/disk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								pkg/system/stats/disk.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| package stats | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
|  | ||||
| 	"github.com/shirou/gopsutil/v3/disk" | ||||
| ) | ||||
|  | ||||
| // DiskInfo represents information about a disk | ||||
| type DiskInfo struct { | ||||
| 	Path        string  `json:"path"` | ||||
| 	Total       float64 `json:"total_gb"` | ||||
| 	Free        float64 `json:"free_gb"` | ||||
| 	Used        float64 `json:"used_gb"` | ||||
| 	UsedPercent float64 `json:"used_percent"` | ||||
| } | ||||
|  | ||||
| // DiskStats contains information about all disks | ||||
| type DiskStats struct { | ||||
| 	Disks []DiskInfo `json:"disks"` | ||||
| } | ||||
|  | ||||
| // GetDiskStats returns information about all disks | ||||
| func GetDiskStats() (*DiskStats, error) { | ||||
| 	partitions, err := disk.Partitions(false) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get disk partitions: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	stats := &DiskStats{ | ||||
| 		Disks: make([]DiskInfo, 0, len(partitions)), | ||||
| 	} | ||||
|  | ||||
| 	for _, partition := range partitions { | ||||
| 		usage, err := disk.Usage(partition.Mountpoint) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Convert bytes to GB and round to 1 decimal place | ||||
| 		totalGB := math.Round(float64(usage.Total)/(1024*1024*1024)*10) / 10 | ||||
| 		freeGB := math.Round(float64(usage.Free)/(1024*1024*1024)*10) / 10 | ||||
| 		usedGB := math.Round(float64(usage.Used)/(1024*1024*1024)*10) / 10 | ||||
|  | ||||
| 		stats.Disks = append(stats.Disks, DiskInfo{ | ||||
| 			Path:        partition.Mountpoint, | ||||
| 			Total:       totalGB, | ||||
| 			Free:        freeGB, | ||||
| 			Used:        usedGB, | ||||
| 			UsedPercent: math.Round(usage.UsedPercent*10) / 10, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return stats, nil | ||||
| } | ||||
|  | ||||
| // GetRootDiskInfo returns information about the root disk | ||||
| func GetRootDiskInfo() (*DiskInfo, error) { | ||||
| 	usage, err := disk.Usage("/") | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get root disk usage: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Convert bytes to GB and round to 1 decimal place | ||||
| 	totalGB := math.Round(float64(usage.Total)/(1024*1024*1024)*10) / 10 | ||||
| 	freeGB := math.Round(float64(usage.Free)/(1024*1024*1024)*10) / 10 | ||||
| 	usedGB := math.Round(float64(usage.Used)/(1024*1024*1024)*10) / 10 | ||||
|  | ||||
| 	return &DiskInfo{ | ||||
| 		Path:        "/", | ||||
| 		Total:       totalGB, | ||||
| 		Free:        freeGB, | ||||
| 		Used:        usedGB, | ||||
| 		UsedPercent: math.Round(usage.UsedPercent*10) / 10, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // GetFormattedDiskInfo returns a formatted string with disk information | ||||
| func GetFormattedDiskInfo() string { | ||||
| 	diskInfo, err := GetRootDiskInfo() | ||||
| 	if err != nil { | ||||
| 		return "Unknown" | ||||
| 	} | ||||
| 	 | ||||
| 	return fmt.Sprintf("%.0fGB (%.0fGB free)", diskInfo.Total, diskInfo.Free) | ||||
| } | ||||
							
								
								
									
										595
									
								
								pkg/system/stats/manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										595
									
								
								pkg/system/stats/manager.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,595 @@ | ||||
| package stats | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/redis/go-redis/v9" | ||||
| ) | ||||
|  | ||||
| // StatsManager is a factory for managing system statistics with caching | ||||
| type StatsManager struct { | ||||
| 	// Redis client for caching | ||||
| 	redisClient *redis.Client | ||||
|  | ||||
| 	// Expiration times for different types of stats in seconds | ||||
| 	Expiration map[string]time.Duration | ||||
|  | ||||
| 	// Debug mode - if true, requests are direct without caching | ||||
| 	Debug bool | ||||
|  | ||||
| 	// Queue for requesting stats updates | ||||
| 	updateQueue chan string | ||||
|  | ||||
| 	// Mutex for thread-safe operations | ||||
| 	mu sync.Mutex | ||||
|  | ||||
| 	// Context for controlling the background goroutine | ||||
| 	ctx    context.Context | ||||
| 	cancel context.CancelFunc | ||||
| 	 | ||||
| 	// Default timeout for waiting for stats | ||||
| 	defaultTimeout time.Duration | ||||
| 	 | ||||
| 	// Logger for StatsManager operations | ||||
| 	logger *log.Logger | ||||
| } | ||||
|  | ||||
| // NewStatsManager creates a new StatsManager with Redis connection | ||||
| func NewStatsManager(config *Config) (*StatsManager, error) { | ||||
| 	// Use default config if nil is provided | ||||
| 	if config == nil { | ||||
| 		config = DefaultConfig() | ||||
| 	} | ||||
|  | ||||
| 	// Create Redis client | ||||
| 	client := redis.NewClient(&redis.Options{ | ||||
| 		Addr:     config.RedisAddr, | ||||
| 		Password: config.RedisPassword, | ||||
| 		DB:       config.RedisDB, | ||||
| 	}) | ||||
|  | ||||
| 	// Test connection | ||||
| 	ctx := context.Background() | ||||
| 	_, err := client.Ping(ctx).Result() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to connect to Redis: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Create context with cancel for the background goroutine | ||||
| 	ctx, cancel := context.WithCancel(ctx) | ||||
|  | ||||
| 	// Create logger | ||||
| 	logger := log.New(os.Stdout, "[StatsManager] ", log.LstdFlags) | ||||
|  | ||||
| 	// Create the manager | ||||
| 	manager := &StatsManager{ | ||||
| 		redisClient:    client, | ||||
| 		Expiration:     config.ExpirationTimes, | ||||
| 		Debug:          config.Debug, | ||||
| 		updateQueue:    make(chan string, config.QueueSize), | ||||
| 		ctx:            ctx, | ||||
| 		cancel:         cancel, | ||||
| 		defaultTimeout: config.DefaultTimeout, | ||||
| 		logger:         logger, | ||||
| 	} | ||||
|  | ||||
| 	// Start the background goroutine for updates | ||||
| 	go manager.updateWorker() | ||||
|  | ||||
| 	// Initialize cache with first fetch | ||||
| 	manager.initializeCache() | ||||
|  | ||||
| 	return manager, nil | ||||
| } | ||||
|  | ||||
| // NewStatsManagerWithDefaults creates a new StatsManager with default settings | ||||
| func NewStatsManagerWithDefaults() (*StatsManager, error) { | ||||
| 	return NewStatsManager(DefaultConfig()) | ||||
| } | ||||
|  | ||||
| // Close closes the StatsManager and its connections | ||||
| func (sm *StatsManager) Close() error { | ||||
| 	// Stop the background goroutine | ||||
| 	sm.cancel() | ||||
|  | ||||
| 	// Close Redis connection | ||||
| 	return sm.redisClient.Close() | ||||
| } | ||||
|  | ||||
| // updateWorker is a background goroutine that processes update requests | ||||
| func (sm *StatsManager) updateWorker() { | ||||
| 	sm.logger.Println("Starting stats update worker goroutine") | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-sm.ctx.Done(): | ||||
| 			// Context cancelled, exit the goroutine | ||||
| 			sm.logger.Println("Stopping stats update worker goroutine") | ||||
| 			return | ||||
| 		case statsType := <-sm.updateQueue: | ||||
| 			// Process the update request | ||||
| 			sm.logger.Printf("Processing update request for %s stats", statsType) | ||||
| 			sm.fetchAndCacheStats(statsType) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // fetchAndCacheStats fetches stats and caches them in Redis | ||||
| func (sm *StatsManager) fetchAndCacheStats(statsType string) { | ||||
| 	var data interface{} | ||||
| 	var err error | ||||
| 	 | ||||
| 	sm.logger.Printf("Fetching %s stats", statsType) | ||||
| 	startTime := time.Now() | ||||
| 	 | ||||
| 	// Fetch the requested stats | ||||
| 	switch statsType { | ||||
| 	case "system": | ||||
| 		data, err = GetSystemInfo() | ||||
| 	case "disk": | ||||
| 		data, err = GetDiskStats() | ||||
| 	case "process": | ||||
| 		data, err = GetProcessStats(0) // Get all processes | ||||
| 	case "root_disk": | ||||
| 		data, err = GetRootDiskInfo() | ||||
| 	case "network": | ||||
| 		data = GetNetworkSpeedResult() | ||||
| 	case "hardware": | ||||
| 		data = GetHardwareStatsJSON() | ||||
| 	default: | ||||
| 		sm.logger.Printf("Unknown stats type: %s", statsType) | ||||
| 		return // Unknown stats type | ||||
| 	} | ||||
| 	 | ||||
| 	if err != nil { | ||||
| 		// Log error but continue | ||||
| 		sm.logger.Printf("Error fetching %s stats: %v", statsType, err) | ||||
| 		return | ||||
| 	} | ||||
| 	 | ||||
| 	// Marshal to JSON | ||||
| 	jsonData, err := json.Marshal(data) | ||||
| 	if err != nil { | ||||
| 		sm.logger.Printf("Error marshaling %s stats: %v", statsType, err) | ||||
| 		return | ||||
| 	} | ||||
| 	 | ||||
| 	// Cache in Redis | ||||
| 	key := fmt.Sprintf("stats:%s", statsType) | ||||
| 	err = sm.redisClient.Set(sm.ctx, key, jsonData, sm.Expiration[statsType]).Err() | ||||
| 	if err != nil { | ||||
| 		sm.logger.Printf("Error caching %s stats: %v", statsType, err) | ||||
| 		return | ||||
| 	} | ||||
| 	 | ||||
| 	// Set last update time | ||||
| 	lastUpdateKey := fmt.Sprintf("stats:%s:last_update", statsType) | ||||
| 	sm.redisClient.Set(sm.ctx, lastUpdateKey, time.Now().Unix(), 0) | ||||
| 	 | ||||
| 	sm.logger.Printf("Successfully cached %s stats in %v", statsType, time.Since(startTime)) | ||||
| } | ||||
|  | ||||
| // initializeCache initializes the cache with initial values | ||||
| func (sm *StatsManager) initializeCache() { | ||||
| 	sm.logger.Println("Initializing stats cache") | ||||
| 	 | ||||
| 	// Queue initial fetches for all stats types | ||||
| 	statsTypes := []string{"system", "disk", "process", "root_disk", "network", "hardware"} | ||||
| 	for _, statsType := range statsTypes { | ||||
| 		sm.logger.Printf("Queueing initial fetch for %s stats", statsType) | ||||
| 		sm.updateQueue <- statsType | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getFromCache gets stats from cache or triggers an update if expired | ||||
| func (sm *StatsManager) getFromCache(statsType string, result interface{}) error { | ||||
| 	// In debug mode, fetch directly without caching | ||||
| 	if sm.Debug { | ||||
| 		sm.logger.Printf("Debug mode enabled, fetching %s stats directly", statsType) | ||||
| 		return sm.fetchDirect(statsType, result) | ||||
| 	} | ||||
| 	 | ||||
| 	key := fmt.Sprintf("stats:%s", statsType) | ||||
| 	sm.logger.Printf("Getting %s stats from cache", statsType) | ||||
| 	 | ||||
| 	// Get from Redis | ||||
| 	jsonData, err := sm.redisClient.Get(sm.ctx, key).Bytes() | ||||
| 	if err == redis.Nil { | ||||
| 		// Not in cache, fetch directly and wait | ||||
| 		sm.logger.Printf("%s stats not found in cache, fetching directly", statsType) | ||||
| 		return sm.fetchDirectAndCache(statsType, result) | ||||
| 	} else if err != nil { | ||||
| 		sm.logger.Printf("Redis error when getting %s stats: %v", statsType, err) | ||||
| 		return fmt.Errorf("redis error: %w", err) | ||||
| 	} | ||||
| 	 | ||||
| 	// Unmarshal the data | ||||
| 	if err := json.Unmarshal(jsonData, result); err != nil { | ||||
| 		sm.logger.Printf("Error unmarshaling %s stats: %v", statsType, err) | ||||
| 		return fmt.Errorf("error unmarshaling data: %w", err) | ||||
| 	} | ||||
| 	 | ||||
| 	// Check if data is expired | ||||
| 	lastUpdateKey := fmt.Sprintf("stats:%s:last_update", statsType) | ||||
| 	lastUpdateStr, err := sm.redisClient.Get(sm.ctx, lastUpdateKey).Result() | ||||
| 	if err == nil { | ||||
| 		var lastUpdate int64 | ||||
| 		fmt.Sscanf(lastUpdateStr, "%d", &lastUpdate) | ||||
| 		 | ||||
| 		// If expired, queue an update for next time | ||||
| 		expiration := sm.Expiration[statsType] | ||||
| 		updateTime := time.Unix(lastUpdate, 0) | ||||
| 		age := time.Since(updateTime) | ||||
| 		 | ||||
| 		sm.logger.Printf("%s stats age: %v (expiration: %v)", statsType, age, expiration) | ||||
| 		 | ||||
| 		if age > expiration { | ||||
| 			sm.logger.Printf("%s stats expired, queueing update for next request", statsType) | ||||
| 			// Queue update for next request | ||||
| 			select { | ||||
| 			case sm.updateQueue <- statsType: | ||||
| 				// Successfully queued | ||||
| 				sm.logger.Printf("Successfully queued %s stats update", statsType) | ||||
| 			default: | ||||
| 				// Queue is full, skip update | ||||
| 				sm.logger.Printf("Update queue full, skipping %s stats update", statsType) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // fetchDirect fetches stats directly without caching | ||||
| func (sm *StatsManager) fetchDirect(statsType string, result interface{}) error { | ||||
| 	var data interface{} | ||||
| 	var err error | ||||
|  | ||||
| 	// Fetch the requested stats | ||||
| 	switch statsType { | ||||
| 	case "system": | ||||
| 		data, err = GetSystemInfo() | ||||
| 	case "disk": | ||||
| 		data, err = GetDiskStats() | ||||
| 	case "process": | ||||
| 		data, err = GetProcessStats(0) // Get all processes | ||||
| 	case "root_disk": | ||||
| 		data, err = GetRootDiskInfo() | ||||
| 	case "network": | ||||
| 		data = GetNetworkSpeedResult() | ||||
| 	case "hardware": | ||||
| 		data = GetHardwareStatsJSON() | ||||
| 	default: | ||||
| 		return fmt.Errorf("unknown stats type: %s", statsType) | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Convert data to the expected type | ||||
| 	jsonData, err := json.Marshal(data) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return json.Unmarshal(jsonData, result) | ||||
| } | ||||
|  | ||||
| // fetchDirectAndCache fetches stats directly and caches them | ||||
| func (sm *StatsManager) fetchDirectAndCache(statsType string, result interface{}) error { | ||||
| 	var data interface{} | ||||
| 	var err error | ||||
|  | ||||
| 	// Fetch the requested stats | ||||
| 	switch statsType { | ||||
| 	case "system": | ||||
| 		data, err = GetSystemInfo() | ||||
| 	case "disk": | ||||
| 		data, err = GetDiskStats() | ||||
| 	case "process": | ||||
| 		data, err = GetProcessStats(0) // Get all processes | ||||
| 	case "root_disk": | ||||
| 		data, err = GetRootDiskInfo() | ||||
| 	case "network": | ||||
| 		data = GetNetworkSpeedResult() | ||||
| 	case "hardware": | ||||
| 		data = GetHardwareStatsJSON() | ||||
| 	default: | ||||
| 		return fmt.Errorf("unknown stats type: %s", statsType) | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Convert data to the expected type | ||||
| 	jsonData, err := json.Marshal(data) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Cache in Redis | ||||
| 	key := fmt.Sprintf("stats:%s", statsType) | ||||
| 	err = sm.redisClient.Set(sm.ctx, key, jsonData, sm.Expiration[statsType]).Err() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Set last update time | ||||
| 	lastUpdateKey := fmt.Sprintf("stats:%s:last_update", statsType) | ||||
| 	sm.redisClient.Set(sm.ctx, lastUpdateKey, time.Now().Unix(), 0) | ||||
|  | ||||
| 	return json.Unmarshal(jsonData, result) | ||||
| } | ||||
|  | ||||
| // waitForCachedData waits for data to be available in cache with timeout | ||||
| func (sm *StatsManager) waitForCachedData(statsType string, timeout time.Duration) bool { | ||||
| 	key := fmt.Sprintf("stats:%s", statsType) | ||||
| 	startTime := time.Now() | ||||
|  | ||||
| 	sm.logger.Printf("Waiting for %s stats to be available in cache (timeout: %v)", statsType, timeout) | ||||
|  | ||||
| 	for { | ||||
| 		// Check if data exists | ||||
| 		exists, err := sm.redisClient.Exists(sm.ctx, key).Result() | ||||
| 		if err == nil && exists > 0 { | ||||
| 			sm.logger.Printf("%s stats found in cache after %v", statsType, time.Since(startTime)) | ||||
| 			return true | ||||
| 		} | ||||
|  | ||||
| 		// Check timeout | ||||
| 		if time.Since(startTime) > timeout { | ||||
| 			sm.logger.Printf("Timeout waiting for %s stats in cache", statsType) | ||||
| 			return false | ||||
| 		} | ||||
|  | ||||
| 		// Wait a bit before checking again | ||||
| 		time.Sleep(100 * time.Millisecond) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ClearCache clears all cached stats or a specific stats type | ||||
| func (sm *StatsManager) ClearCache(statsType string) error { | ||||
| 	sm.mu.Lock() | ||||
| 	defer sm.mu.Unlock() | ||||
|  | ||||
| 	if statsType == "" { | ||||
| 		// Clear all stats | ||||
| 		sm.logger.Println("Clearing all cached stats") | ||||
| 		statsTypes := []string{"system", "disk", "process", "root_disk", "network", "hardware"} | ||||
| 		for _, t := range statsTypes { | ||||
| 			key := fmt.Sprintf("stats:%s", t) | ||||
| 			lastUpdateKey := fmt.Sprintf("stats:%s:last_update", t) | ||||
|  | ||||
| 			sm.redisClient.Del(sm.ctx, key) | ||||
| 			sm.redisClient.Del(sm.ctx, lastUpdateKey) | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Clear specific stats type | ||||
| 		sm.logger.Printf("Clearing cached %s stats", statsType) | ||||
| 		key := fmt.Sprintf("stats:%s", statsType) | ||||
| 		lastUpdateKey := fmt.Sprintf("stats:%s:last_update", statsType) | ||||
|  | ||||
| 		sm.redisClient.Del(sm.ctx, key) | ||||
| 		sm.redisClient.Del(sm.ctx, lastUpdateKey) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ForceUpdate forces an immediate update of stats | ||||
| func (sm *StatsManager) ForceUpdate(statsType string) error { | ||||
| 	sm.logger.Printf("Forcing immediate update of %s stats", statsType) | ||||
|  | ||||
| 	// Clear the cache for this stats type | ||||
| 	err := sm.ClearCache(statsType) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Fetch and cache directly | ||||
| 	switch statsType { | ||||
| 	case "system", "disk", "process", "root_disk", "network", "hardware": | ||||
| 		sm.fetchAndCacheStats(statsType) | ||||
| 		return nil | ||||
| 	default: | ||||
| 		return fmt.Errorf("unknown stats type: %s", statsType) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetSystemInfo gets system information with caching | ||||
| func (sm *StatsManager) GetSystemInfo() (*SystemInfo, error) { | ||||
| 	var result SystemInfo | ||||
|  | ||||
| 	// Try to get from cache | ||||
| 	err := sm.getFromCache("system", &result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // GetDiskStats gets disk statistics with caching | ||||
| func (sm *StatsManager) GetDiskStats() (*DiskStats, error) { | ||||
| 	var result DiskStats | ||||
|  | ||||
| 	// Try to get from cache | ||||
| 	err := sm.getFromCache("disk", &result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // GetRootDiskInfo gets root disk information with caching | ||||
| func (sm *StatsManager) GetRootDiskInfo() (*DiskInfo, error) { | ||||
| 	var result DiskInfo | ||||
|  | ||||
| 	// Try to get from cache | ||||
| 	err := sm.getFromCache("root_disk", &result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // GetProcessStats gets process statistics with caching | ||||
| func (sm *StatsManager) GetProcessStats(limit int) (*ProcessStats, error) { | ||||
| 	var result ProcessStats | ||||
|  | ||||
| 	// Try to get from cache | ||||
| 	err := sm.getFromCache("process", &result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Apply limit if needed | ||||
| 	if limit > 0 && len(result.Processes) > limit { | ||||
| 		result.Processes = result.Processes[:limit] | ||||
| 	} | ||||
|  | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // GetProcessStatsFresh gets fresh process statistics bypassing the cache | ||||
| func (sm *StatsManager) GetProcessStatsFresh(limit int) (*ProcessStats, error) { | ||||
| 	var result ProcessStats | ||||
|  | ||||
| 	// Get fresh data and update cache | ||||
| 	err := sm.fetchDirectAndCache("process", &result) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Apply limit if needed | ||||
| 	if limit > 0 && len(result.Processes) > limit { | ||||
| 		result.Processes = result.Processes[:limit] | ||||
| 	} | ||||
|  | ||||
| 	// Log that we're bypassing cache | ||||
| 	sm.logger.Printf("Bypassing cache for process stats with manual refresh") | ||||
| 	 | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // GetTopProcesses gets top processes by CPU usage with caching | ||||
| func (sm *StatsManager) GetTopProcesses(n int) ([]ProcessInfo, error) { | ||||
| 	stats, err := sm.GetProcessStats(n) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return stats.Processes, nil | ||||
| } | ||||
|  | ||||
| // GetNetworkSpeedResult gets network speed with caching | ||||
| func (sm *StatsManager) GetNetworkSpeedResult() NetworkSpeedResult { | ||||
| 	var result NetworkSpeedResult | ||||
|  | ||||
| 	// Try to get from cache | ||||
| 	err := sm.getFromCache("network", &result) | ||||
| 	if err != nil { | ||||
| 		// Fallback to direct fetch on error | ||||
| 		uploadSpeed, downloadSpeed := GetNetworkSpeed() | ||||
| 		return NetworkSpeedResult{ | ||||
| 			UploadSpeed:   uploadSpeed, | ||||
| 			DownloadSpeed: downloadSpeed, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // GetHardwareStats gets hardware statistics with caching | ||||
| func (sm *StatsManager) GetHardwareStats() map[string]interface{} { | ||||
| 	var result map[string]interface{} | ||||
|  | ||||
| 	// Try to get from cache | ||||
| 	err := sm.getFromCache("hardware", &result) | ||||
| 	if err != nil { | ||||
| 		// Fallback to direct fetch on error | ||||
| 		return GetHardwareStats() | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // GetHardwareStatsJSON gets hardware statistics in JSON format with caching | ||||
| func (sm *StatsManager) GetHardwareStatsJSON() map[string]interface{} { | ||||
| 	var result map[string]interface{} | ||||
|  | ||||
| 	// Try to get from cache | ||||
| 	err := sm.getFromCache("hardware", &result) | ||||
| 	if err != nil { | ||||
| 		// Fallback to direct fetch on error | ||||
| 		return GetHardwareStatsJSON() | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // GetFormattedCPUInfo gets formatted CPU info with caching | ||||
| func (sm *StatsManager) GetFormattedCPUInfo() string { | ||||
| 	sysInfo, err := sm.GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		return "Unknown" | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%d cores (%s)", sysInfo.CPU.Cores, sysInfo.CPU.ModelName) | ||||
| } | ||||
|  | ||||
| // GetFormattedMemoryInfo gets formatted memory info with caching | ||||
| func (sm *StatsManager) GetFormattedMemoryInfo() string { | ||||
| 	sysInfo, err := sm.GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		return "Unknown" | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%.1fGB (%.1fGB used)", sysInfo.Memory.Total, sysInfo.Memory.Used) | ||||
| } | ||||
|  | ||||
| // GetFormattedDiskInfo gets formatted disk info with caching | ||||
| func (sm *StatsManager) GetFormattedDiskInfo() string { | ||||
| 	diskInfo, err := sm.GetRootDiskInfo() | ||||
| 	if err != nil { | ||||
| 		return "Unknown" | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%.0fGB (%.0fGB free)", diskInfo.Total, diskInfo.Free) | ||||
| } | ||||
|  | ||||
| // GetFormattedNetworkInfo gets formatted network info with caching | ||||
| func (sm *StatsManager) GetFormattedNetworkInfo() string { | ||||
| 	netSpeed := sm.GetNetworkSpeedResult() | ||||
|  | ||||
| 	return fmt.Sprintf("Up: %s\nDown: %s", netSpeed.UploadSpeed, netSpeed.DownloadSpeed) | ||||
| } | ||||
|  | ||||
| // GetProcessStatsJSON gets process statistics in JSON format with caching | ||||
| func (sm *StatsManager) GetProcessStatsJSON(limit int) map[string]interface{} { | ||||
| 	// Get process stats | ||||
| 	processStats, err := sm.GetProcessStats(limit) | ||||
| 	if err != nil { | ||||
| 		return map[string]interface{}{ | ||||
| 			"processes": []interface{}{}, | ||||
| 			"total":     0, | ||||
| 			"filtered":  0, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Convert to JSON-friendly format | ||||
| 	return map[string]interface{}{ | ||||
| 		"processes": processStats.Processes, | ||||
| 		"total":     processStats.Total, | ||||
| 		"filtered":  processStats.Filtered, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										3
									
								
								pkg/system/stats/package.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								pkg/system/stats/package.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| // Package stats provides functions for measuring system statistics including CPU, memory, disk, and processes. | ||||
| package stats | ||||
|  | ||||
							
								
								
									
										165
									
								
								pkg/system/stats/process.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								pkg/system/stats/process.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| package stats | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/shirou/gopsutil/v3/process" | ||||
| ) | ||||
|  | ||||
| // ProcessInfo contains information about a single process | ||||
| type ProcessInfo struct { | ||||
| 	PID         int32   `json:"pid"` | ||||
| 	Name        string  `json:"name"` | ||||
| 	Status      string  `json:"status"` | ||||
| 	CPUPercent  float64 `json:"cpu_percent"` | ||||
| 	MemoryMB    float64 `json:"memory_mb"` | ||||
| 	CreateTime  string  `json:"create_time"` | ||||
| 	IsCurrent   bool    `json:"is_current"` | ||||
| 	CommandLine string  `json:"command_line,omitempty"` | ||||
| } | ||||
|  | ||||
| // ProcessStats contains information about all processes | ||||
| type ProcessStats struct { | ||||
| 	Processes []ProcessInfo `json:"processes"` | ||||
| 	Total     int           `json:"total"` | ||||
| 	Filtered  int           `json:"filtered"` | ||||
| } | ||||
|  | ||||
| // GetProcessStats returns information about all processes | ||||
| func GetProcessStats(limit int) (*ProcessStats, error) { | ||||
| 	// Get process information | ||||
| 	processes, err := process.Processes() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to get processes: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// Process data to return | ||||
| 	stats := &ProcessStats{ | ||||
| 		Processes: make([]ProcessInfo, 0, len(processes)), | ||||
| 		Total:     len(processes), | ||||
| 	} | ||||
|  | ||||
| 	// Current process ID | ||||
| 	currentPid := int32(os.Getpid()) | ||||
|  | ||||
| 	// Get stats for each process | ||||
| 	for _, p := range processes { | ||||
| 		// Skip processes we can't access | ||||
| 		name, err := p.Name() | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Get memory info | ||||
| 		memInfo, err := p.MemoryInfo() | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Get CPU percent | ||||
| 		cpuPercent, err := p.CPUPercent() | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Skip processes with very minimal CPU and memory usage to reduce data size | ||||
| 		if cpuPercent < 0.01 && float64(memInfo.RSS)/(1024*1024) < 1 { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Get process status | ||||
| 		status := "unknown" | ||||
| 		statusSlice, err := p.Status() | ||||
| 		if err == nil && len(statusSlice) > 0 { | ||||
| 			status = statusSlice[0] | ||||
| 		} | ||||
|  | ||||
| 		// Get process creation time | ||||
| 		createTime, err := p.CreateTime() | ||||
| 		if err != nil { | ||||
| 			createTime = 0 | ||||
| 		} | ||||
|  | ||||
| 		// Format creation time as string | ||||
| 		createTimeStr := "N/A" | ||||
| 		if createTime > 0 { | ||||
| 			createTimeStr = time.Unix(createTime/1000, 0).Format("2006-01-02 15:04:05") | ||||
| 		} | ||||
|  | ||||
| 		// Calculate memory in MB and round to 1 decimal place | ||||
| 		memoryMB := math.Round(float64(memInfo.RSS)/(1024*1024)*10) / 10 | ||||
|  | ||||
| 		// Round CPU percent to 1 decimal place | ||||
| 		cpuPercent = math.Round(cpuPercent*10) / 10 | ||||
|  | ||||
| 		// Check if this is the current process | ||||
| 		isCurrent := p.Pid == currentPid | ||||
|  | ||||
| 		// Get command line (may be empty if no permission) | ||||
| 		cmdline := "" | ||||
| 		cmdlineSlice, err := p.Cmdline() | ||||
| 		if err == nil { | ||||
| 			cmdline = cmdlineSlice | ||||
| 		} | ||||
|  | ||||
| 		// Add process info | ||||
| 		stats.Processes = append(stats.Processes, ProcessInfo{ | ||||
| 			PID:         p.Pid, | ||||
| 			Name:        name, | ||||
| 			Status:      status, | ||||
| 			CPUPercent:  cpuPercent, | ||||
| 			MemoryMB:    memoryMB, | ||||
| 			CreateTime:  createTimeStr, | ||||
| 			IsCurrent:   isCurrent, | ||||
| 			CommandLine: cmdline, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	stats.Filtered = len(stats.Processes) | ||||
|  | ||||
| 	// Sort processes by CPU usage (descending) | ||||
| 	sort.Slice(stats.Processes, func(i, j int) bool { | ||||
| 		return stats.Processes[i].CPUPercent > stats.Processes[j].CPUPercent | ||||
| 	}) | ||||
|  | ||||
| 	// Limit to top N processes if requested | ||||
| 	if limit > 0 && len(stats.Processes) > limit { | ||||
| 		stats.Processes = stats.Processes[:limit] | ||||
| 	} | ||||
|  | ||||
| 	return stats, nil | ||||
| } | ||||
|  | ||||
| // GetTopProcesses returns the top N processes by CPU usage | ||||
| func GetTopProcesses(n int) ([]ProcessInfo, error) { | ||||
| 	stats, err := GetProcessStats(n) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	 | ||||
| 	return stats.Processes, nil | ||||
| } | ||||
|  | ||||
| // GetProcessStatsJSON returns process statistics in a format suitable for JSON responses | ||||
| func GetProcessStatsJSON(limit int) map[string]interface{} { | ||||
| 	// Get process stats | ||||
| 	processStats, err := GetProcessStats(limit) | ||||
| 	if err != nil { | ||||
| 		return map[string]interface{}{ | ||||
| 			"processes": []interface{}{}, | ||||
| 			"total":     0, | ||||
| 			"filtered":  0, | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	// Convert to JSON-friendly format | ||||
| 	return map[string]interface{}{ | ||||
| 		"processes": processStats.Processes, | ||||
| 		"total":     processStats.Total, | ||||
| 		"filtered":  processStats.Filtered, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										241
									
								
								pkg/system/stats/system.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								pkg/system/stats/system.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| package stats | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"runtime" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/shirou/gopsutil/v3/cpu" | ||||
| 	"github.com/shirou/gopsutil/v3/mem" | ||||
| 	"github.com/shirou/gopsutil/v3/net" | ||||
| ) | ||||
|  | ||||
| // SystemInfo contains information about the system's CPU and memory | ||||
| type SystemInfo struct { | ||||
| 	CPU     CPUInfo     `json:"cpu"` | ||||
| 	Memory  MemoryInfo  `json:"memory"` | ||||
| 	Network NetworkInfo `json:"network"` | ||||
| } | ||||
|  | ||||
| // CPUInfo contains information about the CPU | ||||
| type CPUInfo struct { | ||||
| 	Cores       int     `json:"cores"` | ||||
| 	ModelName   string  `json:"model_name"` | ||||
| 	UsagePercent float64 `json:"usage_percent"` | ||||
| } | ||||
|  | ||||
| // MemoryInfo contains information about the system memory | ||||
| type MemoryInfo struct { | ||||
| 	Total       float64 `json:"total_gb"` | ||||
| 	Used        float64 `json:"used_gb"` | ||||
| 	Free        float64 `json:"free_gb"` | ||||
| 	UsedPercent float64 `json:"used_percent"` | ||||
| } | ||||
|  | ||||
| // NetworkInfo contains information about network usage | ||||
| type NetworkInfo struct { | ||||
| 	UploadSpeed   string `json:"upload_speed"` | ||||
| 	DownloadSpeed string `json:"download_speed"` | ||||
| 	BytesSent     uint64 `json:"bytes_sent"` | ||||
| 	BytesReceived uint64 `json:"bytes_received"` | ||||
| } | ||||
|  | ||||
| // NetworkSpeedResult contains the upload and download speeds | ||||
| type NetworkSpeedResult struct { | ||||
| 	UploadSpeed   string `json:"upload_speed"` | ||||
| 	DownloadSpeed string `json:"download_speed"` | ||||
| } | ||||
|  | ||||
| // UptimeProvider defines an interface for getting system uptime | ||||
| type UptimeProvider interface { | ||||
| 	GetUptime() string | ||||
| } | ||||
|  | ||||
| // GetSystemInfo returns information about the system's CPU and memory | ||||
| func GetSystemInfo() (*SystemInfo, error) { | ||||
| 	// Get CPU info | ||||
| 	cpuInfo := CPUInfo{ | ||||
| 		Cores:     runtime.NumCPU(), | ||||
| 		ModelName: "Unknown", | ||||
| 	} | ||||
| 	 | ||||
| 	// Try to get detailed CPU info | ||||
| 	info, err := cpu.Info() | ||||
| 	if err == nil && len(info) > 0 { | ||||
| 		cpuInfo.ModelName = info[0].ModelName | ||||
| 	} | ||||
| 	 | ||||
| 	// Get CPU usage | ||||
| 	cpuPercent, err := cpu.Percent(time.Second, false) | ||||
| 	if err == nil && len(cpuPercent) > 0 { | ||||
| 		cpuInfo.UsagePercent = math.Round(cpuPercent[0]*10) / 10 | ||||
| 	} | ||||
| 	 | ||||
| 	// Get memory info | ||||
| 	memInfo := MemoryInfo{} | ||||
| 	virtualMem, err := mem.VirtualMemory() | ||||
| 	if err == nil { | ||||
| 		memInfo.Total = float64(virtualMem.Total) / (1024 * 1024 * 1024) // Convert to GB | ||||
| 		memInfo.Used = float64(virtualMem.Used) / (1024 * 1024 * 1024) | ||||
| 		memInfo.Free = float64(virtualMem.Free) / (1024 * 1024 * 1024) | ||||
| 		memInfo.UsedPercent = math.Round(virtualMem.UsedPercent*10) / 10 | ||||
| 	} | ||||
| 	 | ||||
| 	// Get network speed | ||||
| 	uploadSpeed, downloadSpeed := GetNetworkSpeed() | ||||
| 	 | ||||
| 	// Create and return the system info | ||||
| 	return &SystemInfo{ | ||||
| 		CPU:    cpuInfo, | ||||
| 		Memory: memInfo, | ||||
| 		Network: NetworkInfo{ | ||||
| 			UploadSpeed:   uploadSpeed, | ||||
| 			DownloadSpeed: downloadSpeed, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // GetNetworkSpeed returns the current network speed in Mbps | ||||
| func GetNetworkSpeed() (string, string) { | ||||
| 	networkUpSpeed := "Unknown" | ||||
| 	networkDownSpeed := "Unknown" | ||||
|  | ||||
| 	// Get initial network counters | ||||
| 	countersStart, err := net.IOCounters(false) | ||||
| 	if err != nil || len(countersStart) == 0 { | ||||
| 		return networkUpSpeed, networkDownSpeed | ||||
| 	} | ||||
|  | ||||
| 	// Wait a short time to measure the difference | ||||
| 	time.Sleep(500 * time.Millisecond) | ||||
|  | ||||
| 	// Get updated network counters | ||||
| 	countersEnd, err := net.IOCounters(false) | ||||
| 	if err != nil || len(countersEnd) == 0 { | ||||
| 		return networkUpSpeed, networkDownSpeed | ||||
| 	} | ||||
|  | ||||
| 	// Calculate the difference in bytes | ||||
| 	bytesSent := countersEnd[0].BytesSent - countersStart[0].BytesSent | ||||
| 	bytesRecv := countersEnd[0].BytesRecv - countersStart[0].BytesRecv | ||||
|  | ||||
| 	// Convert to Mbps (megabits per second) | ||||
| 	// 500ms = 0.5s, so multiply by 2 to get per second | ||||
| 	// Then convert bytes to bits (*8) and to megabits (/1024/1024) | ||||
| 	mbpsSent := float64(bytesSent) * 2 * 8 / 1024 / 1024 | ||||
| 	mbpsRecv := float64(bytesRecv) * 2 * 8 / 1024 / 1024 | ||||
|  | ||||
| 	// Format the speeds with appropriate units | ||||
| 	if mbpsSent < 1 { | ||||
| 		networkUpSpeed = fmt.Sprintf("%.1f Kbps", mbpsSent*1024) | ||||
| 	} else { | ||||
| 		networkUpSpeed = fmt.Sprintf("%.1f Mbps", mbpsSent) | ||||
| 	} | ||||
|  | ||||
| 	if mbpsRecv < 1 { | ||||
| 		networkDownSpeed = fmt.Sprintf("%.1f Kbps", mbpsRecv*1024) | ||||
| 	} else { | ||||
| 		networkDownSpeed = fmt.Sprintf("%.1f Mbps", mbpsRecv) | ||||
| 	} | ||||
|  | ||||
| 	return networkUpSpeed, networkDownSpeed | ||||
| } | ||||
|  | ||||
| // GetNetworkSpeedResult returns the network speed as a struct | ||||
| func GetNetworkSpeedResult() NetworkSpeedResult { | ||||
| 	uploadSpeed, downloadSpeed := GetNetworkSpeed() | ||||
| 	return NetworkSpeedResult{ | ||||
| 		UploadSpeed:   uploadSpeed, | ||||
| 		DownloadSpeed: downloadSpeed, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetFormattedCPUInfo returns a formatted string with CPU information | ||||
| func GetFormattedCPUInfo() string { | ||||
| 	sysInfo, err := GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		return "Unknown" | ||||
| 	} | ||||
| 	 | ||||
| 	return fmt.Sprintf("%d cores (%s)", sysInfo.CPU.Cores, sysInfo.CPU.ModelName) | ||||
| } | ||||
|  | ||||
| // GetFormattedMemoryInfo returns a formatted string with memory information | ||||
| func GetFormattedMemoryInfo() string { | ||||
| 	sysInfo, err := GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		return "Unknown" | ||||
| 	} | ||||
| 	 | ||||
| 	return fmt.Sprintf("%.1fGB (%.1fGB used)", sysInfo.Memory.Total, sysInfo.Memory.Used) | ||||
| } | ||||
|  | ||||
| // GetFormattedNetworkInfo returns a formatted string with network information | ||||
| func GetFormattedNetworkInfo() string { | ||||
| 	sysInfo, err := GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		return "Unknown" | ||||
| 	} | ||||
| 	 | ||||
| 	return fmt.Sprintf("Up: %s\nDown: %s", sysInfo.Network.UploadSpeed, sysInfo.Network.DownloadSpeed) | ||||
| } | ||||
|  | ||||
| // GetHardwareStats returns a map with hardware statistics | ||||
| func GetHardwareStats() map[string]interface{} { | ||||
| 	// Create the hardware stats map | ||||
| 	hardwareStats := map[string]interface{}{ | ||||
| 		"cpu":     GetFormattedCPUInfo(), | ||||
| 		"memory":  GetFormattedMemoryInfo(), | ||||
| 		"disk":    GetFormattedDiskInfo(), | ||||
| 		"network": GetFormattedNetworkInfo(), | ||||
| 	} | ||||
| 	 | ||||
| 	return hardwareStats | ||||
| } | ||||
|  | ||||
| // GetHardwareStatsJSON returns hardware statistics in a format suitable for JSON responses | ||||
| func GetHardwareStatsJSON() map[string]interface{} { | ||||
| 	// Get system information | ||||
| 	sysInfo, err := GetSystemInfo() | ||||
| 	if err != nil { | ||||
| 		return map[string]interface{}{ | ||||
| 			"error": "Failed to get system info: " + err.Error(), | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	// Get disk information | ||||
| 	diskInfo, err := GetRootDiskInfo() | ||||
| 	if err != nil { | ||||
| 		return map[string]interface{}{ | ||||
| 			"error": "Failed to get disk info: " + err.Error(), | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	// Create the hardware stats map | ||||
| 	hardwareStats := map[string]interface{}{ | ||||
| 		"cpu": map[string]interface{}{ | ||||
| 			"cores":        sysInfo.CPU.Cores, | ||||
| 			"model":        sysInfo.CPU.ModelName, | ||||
| 			"usage_percent": sysInfo.CPU.UsagePercent, | ||||
| 		}, | ||||
| 		"memory": map[string]interface{}{ | ||||
| 			"total_gb":     sysInfo.Memory.Total, | ||||
| 			"used_gb":      sysInfo.Memory.Used, | ||||
| 			"free_gb":      sysInfo.Memory.Free, | ||||
| 			"used_percent": sysInfo.Memory.UsedPercent, | ||||
| 		}, | ||||
| 		"disk": map[string]interface{}{ | ||||
| 			"total_gb":     diskInfo.Total, | ||||
| 			"free_gb":      diskInfo.Free, | ||||
| 			"used_gb":      diskInfo.Used, | ||||
| 			"used_percent": diskInfo.UsedPercent, | ||||
| 		}, | ||||
| 		"network": map[string]interface{}{ | ||||
| 			"upload_speed":   sysInfo.Network.UploadSpeed, | ||||
| 			"download_speed": sysInfo.Network.DownloadSpeed, | ||||
| 		}, | ||||
| 	} | ||||
| 	 | ||||
| 	return hardwareStats | ||||
| } | ||||
		Reference in New Issue
	
	Block a user