...
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