This commit is contained in:
2025-04-23 04:18:28 +02:00
parent 10a7d9bb6b
commit a16ac8f627
276 changed files with 85166 additions and 1 deletions

175
pkg/system/stats/README.md Normal file
View 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
```

View 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 {}
}

View 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
}

View 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!")
}

View 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)
}

View 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
View 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
View 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,
}
}

View 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
View 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
View 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
}