11package system
22
33import (
4+ "context"
45 "fmt"
6+ "math"
57 "os/exec"
68 "runtime"
79 "strconv"
@@ -14,11 +16,13 @@ import (
1416 "github.com/hhftechnology/traefik-log-dashboard/agent/pkg/logger"
1517)
1618
19+ const commandTimeout = 5 * time .Second
20+
1721// SystemInfo represents system information for the API response
1822type SystemInfo struct {
19- Uptime int64 `json:"uptime"`
20- Timestamp string `json:"timestamp"`
21- CPU CPUStats `json:"cpu"`
23+ Uptime int64 `json:"uptime"`
24+ Timestamp string `json:"timestamp"`
25+ CPU CPUStats `json:"cpu"`
2226 Memory MemoryStats `json:"memory"`
2327 Disk DiskStats `json:"disk"`
2428}
@@ -28,7 +32,7 @@ type CPUStats struct {
2832 Model string `json:"model"`
2933 Cores int `json:"cores"`
3034 Speed float64 `json:"speed"`
31- UsagePercent float64 `json:"usage_percent"` // Changed from Usage to UsagePercent
35+ UsagePercent float64 `json:"usage_percent"`
3236 CoreUsage []float64 `json:"coreUsage"`
3337}
3438
@@ -38,24 +42,15 @@ type MemoryStats struct {
3842 Available uint64 `json:"available"`
3943 Used uint64 `json:"used"`
4044 Total uint64 `json:"total"`
41- UsedPercent float64 `json:"used_percent"` // Added percentage calculation
45+ UsedPercent float64 `json:"used_percent"`
4246}
4347
44- // DiskStats represents aggregated disk statistics with percentage
48+ // DiskStats represents root disk statistics with percentage
4549type DiskStats struct {
4650 Total uint64 `json:"total"`
4751 Used uint64 `json:"used"`
4852 Free uint64 `json:"free"`
49- UsedPercent float64 `json:"used_percent"` // Added percentage calculation
50- }
51-
52- // DiskInfo represents individual disk information (internal use)
53- type DiskInfo struct {
54- Filesystem string `json:"filesystem"`
55- Size uint64 `json:"size"`
56- Used uint64 `json:"used"`
57- Free uint64 `json:"free"`
58- MountedOn string `json:"mountedOn"`
53+ UsedPercent float64 `json:"used_percent"`
5954}
6055
6156func MeasureSystem () (SystemInfo , error ) {
@@ -106,25 +101,33 @@ func getCPUStats() (CPUStats, error) {
106101 cpuUsage , err := cpu .Percent (time .Second , true )
107102 if err != nil {
108103 logger .Log .Printf ("Warning: Could not get CPU usage: %v" , err )
109- cpuUsage = make ([]float64 , len ( cpuInfo ))
104+ cpuUsage = make ([]float64 , runtime . NumCPU ( ))
110105 }
111106
112107 model := cpuInfo [0 ].ModelName
113108 cores := len (cpuUsage )
109+
110+ // FIX BUG #2: Ensure cores is never zero
114111 if cores == 0 {
115112 cores = runtime .NumCPU ()
113+ if cores == 0 {
114+ cores = 1 // Absolute minimum fallback
115+ }
116116 cpuUsage = make ([]float64 , cores )
117117 }
118+
118119 speed := cpuInfo [0 ].Mhz
119120
120- // Calculate average usage across all cores
121+ // FIX BUG #2: Safe division - handle empty cpuUsage
121122 var overallUsage float64
122123 if len (cpuUsage ) > 0 {
123124 var total float64
124125 for _ , usage := range cpuUsage {
125126 total += usage
126127 }
127128 overallUsage = total / float64 (len (cpuUsage ))
129+ } else {
130+ overallUsage = 0.0 // Safe default when no CPU usage available
128131 }
129132
130133 return CPUStats {
@@ -149,9 +152,11 @@ func getCPUStatsFallback() (CPUStats, error) {
149152
150153func getCPUInfoMacOS () (CPUStats , error ) {
151154 var stats CPUStats
155+ ctx , cancel := context .WithTimeout (context .Background (), commandTimeout )
156+ defer cancel ()
152157
153158 // Get CPU model
154- cmd := exec .Command ( "sysctl" , "-n" , "machdep.cpu.brand_string" )
159+ cmd := exec .CommandContext ( ctx , "sysctl" , "-n" , "machdep.cpu.brand_string" )
155160 output , err := cmd .Output ()
156161 if err == nil {
157162 stats .Model = strings .TrimSpace (string (output ))
@@ -160,7 +165,7 @@ func getCPUInfoMacOS() (CPUStats, error) {
160165 }
161166
162167 // Get CPU core count
163- cmd = exec .Command ( "sysctl" , "-n" , "hw.ncpu" )
168+ cmd = exec .CommandContext ( ctx , "sysctl" , "-n" , "hw.ncpu" )
164169 output , err = cmd .Output ()
165170 if err == nil {
166171 if cores , err := strconv .Atoi (strings .TrimSpace (string (output ))); err == nil {
@@ -170,23 +175,30 @@ func getCPUInfoMacOS() (CPUStats, error) {
170175 stats .Cores = runtime .NumCPU ()
171176 }
172177
173- // Get CPU frequency
174- freqKeys := []string {"hw.cpufrequency_max" , "hw.cpufrequency" , "machdep.cpu.max_basic" }
175- for _ , key := range freqKeys {
176- cmd = exec .Command ("sysctl" , "-n" , key )
178+ // FIX BUG #4: Improved CPU frequency detection for macOS
179+ // Try hw.cpufrequency_max first (Intel Macs, in Hz)
180+ cmd = exec .CommandContext (ctx , "sysctl" , "-n" , "hw.cpufrequency_max" )
181+ output , err = cmd .Output ()
182+ if err == nil {
183+ if freq , err := strconv .ParseFloat (strings .TrimSpace (string (output )), 64 ); err == nil && freq > 0 {
184+ stats .Speed = freq / 1000000 // Convert Hz to MHz
185+ }
186+ }
187+
188+ // If still zero, try hw.cpufrequency (alternative, also in Hz)
189+ if stats .Speed == 0 {
190+ cmd = exec .CommandContext (ctx , "sysctl" , "-n" , "hw.cpufrequency" )
177191 output , err = cmd .Output ()
178192 if err == nil {
179- if freq , err := strconv .ParseFloat (strings .TrimSpace (string (output )), 64 ); err == nil {
180- if freq > 1000000 {
181- stats .Speed = freq / 1000000
182- } else {
183- stats .Speed = freq
184- }
185- break
193+ if freq , err := strconv .ParseFloat (strings .TrimSpace (string (output )), 64 ); err == nil && freq > 0 {
194+ stats .Speed = freq / 1000000 // Convert Hz to MHz
186195 }
187196 }
188197 }
189198
199+ // For Apple Silicon, frequency is not directly available - leave at 0
200+ // The brand string (stats.Model) will indicate M1/M2/M3 etc.
201+
190202 // Get CPU usage
191203 cpuUsage , err := cpu .Percent (time .Second , false )
192204 if err == nil && len (cpuUsage ) > 0 {
@@ -198,8 +210,10 @@ func getCPUInfoMacOS() (CPUStats, error) {
198210
199211func getCPUInfoWindows () (CPUStats , error ) {
200212 var stats CPUStats
213+ ctx , cancel := context .WithTimeout (context .Background (), commandTimeout )
214+ defer cancel ()
201215
202- cmd := exec .Command ( "wmic" , "cpu" , "get" , "Name,NumberOfCores,MaxClockSpeed" , "/format:csv" )
216+ cmd := exec .CommandContext ( ctx , "wmic" , "cpu" , "get" , "Name,NumberOfCores,MaxClockSpeed" , "/format:csv" )
203217 output , err := cmd .Output ()
204218 if err != nil {
205219 return stats , err
@@ -233,15 +247,20 @@ func getCPUInfoWindows() (CPUStats, error) {
233247
234248func getCPUInfoLinux () (CPUStats , error ) {
235249 var stats CPUStats
250+ ctx , cancel := context .WithTimeout (context .Background (), commandTimeout )
251+ defer cancel ()
236252
237- cmd := exec .Command ( "cat" , "/proc/cpuinfo" )
253+ cmd := exec .CommandContext ( ctx , "cat" , "/proc/cpuinfo" )
238254 output , err := cmd .Output ()
239255 if err != nil {
240256 return stats , err
241257 }
242258
243259 lines := strings .Split (string (output ), "\n " )
244- coreCount := 0
260+ logicalCores := 0
261+ physicalCores := 0
262+
263+ // FIX BUG #5: Correctly count physical cores vs logical processors
245264 for _ , line := range lines {
246265 if strings .HasPrefix (line , "model name" ) {
247266 parts := strings .Split (line , ":" )
@@ -255,11 +274,32 @@ func getCPUInfoLinux() (CPUStats, error) {
255274 stats .Speed = speed
256275 }
257276 }
277+ } else if strings .HasPrefix (line , "cpu cores" ) {
278+ // Physical cores (this is what we want!)
279+ parts := strings .Split (line , ":" )
280+ if len (parts ) >= 2 {
281+ if cores , err := strconv .Atoi (strings .TrimSpace (parts [1 ])); err == nil {
282+ physicalCores = cores
283+ }
284+ }
258285 } else if strings .HasPrefix (line , "processor" ) {
259- coreCount ++
286+ logicalCores ++ // Logical processors (includes hyperthreading)
260287 }
261288 }
262- stats .Cores = coreCount
289+
290+ // Prefer physical cores, fallback to logical
291+ if physicalCores > 0 {
292+ stats .Cores = physicalCores
293+ } else if logicalCores > 0 {
294+ stats .Cores = logicalCores
295+ } else {
296+ stats .Cores = runtime .NumCPU ()
297+ }
298+
299+ // Final safety check
300+ if stats .Cores == 0 {
301+ stats .Cores = 1
302+ }
263303
264304 // Get CPU usage
265305 cpuUsage , err := cpu .Percent (time .Second , false )
@@ -291,88 +331,68 @@ func getMemoryStats() (MemoryStats, error) {
291331 }, nil
292332}
293333
334+ // Updated to report only root (/) filesystem usage for consistent "Disk Usage" metric
294335func getDiskStats () (DiskStats , error ) {
295- disks , err := getDiskInfo ( )
336+ usage , err := disk . Usage ( "/" )
296337 if err != nil {
297- return DiskStats {}, err
298- }
299-
300- if len (disks ) == 0 {
301- return DiskStats {}, fmt .Errorf ("no disk information available" )
302- }
303-
304- // Aggregate all disk stats
305- var totalSize , totalUsed , totalFree uint64
306- for _ , disk := range disks {
307- totalSize += disk .Size
308- totalUsed += disk .Used
309- totalFree += disk .Free
338+ logger .Log .Printf ("Error getting root disk usage: %v" , err )
339+ return DiskStats {}, fmt .Errorf ("failed to get root disk usage: %w" , err )
310340 }
311341
312342 // Calculate used percentage
313343 usedPercent := 0.0
314- if totalSize > 0 {
315- usedPercent = (float64 (totalUsed ) / float64 (totalSize )) * 100.0
344+ if usage . Total > 0 {
345+ usedPercent = (float64 (usage . Used ) / float64 (usage . Total )) * 100.0
316346 }
317347
348+ logger .Log .Printf ("Root disk stats: total=%.2f GB, used=%.2f GB, free=%.2f GB, percent=%.1f%%" ,
349+ float64 (usage .Total )/ 1024 / 1024 / 1024 ,
350+ float64 (usage .Used )/ 1024 / 1024 / 1024 ,
351+ float64 (usage .Free )/ 1024 / 1024 / 1024 ,
352+ usedPercent )
353+
318354 return DiskStats {
319- Total : totalSize ,
320- Used : totalUsed ,
321- Free : totalFree ,
355+ Total : usage . Total ,
356+ Used : usage . Used ,
357+ Free : usage . Free ,
322358 UsedPercent : parseFloat (usedPercent , 1 ),
323359 }, nil
324360}
325361
326- func getDiskInfo () ([]DiskInfo , error ) {
327- var disks []DiskInfo
328-
329- partitions , err := disk .Partitions (false )
330- if err != nil {
331- return nil , err
332- }
333-
334- for _ , partition := range partitions {
335- usage , err := disk .Usage (partition .Mountpoint )
336- if err != nil {
337- logger .Log .Printf ("Error getting disk usage for %s: %v" , partition .Mountpoint , err )
338- continue
339- }
340-
341- disks = append (disks , DiskInfo {
342- Filesystem : partition .Device ,
343- Size : usage .Total ,
344- Used : usage .Used ,
345- Free : usage .Free ,
346- MountedOn : partition .Mountpoint ,
347- })
348- }
349-
350- return disks , nil
351- }
352-
353362func getUptime () (int64 , error ) {
363+ ctx , cancel := context .WithTimeout (context .Background (), commandTimeout )
364+ defer cancel ()
365+
354366 switch runtime .GOOS {
355367 case "windows" :
356- cmd := exec .Command ("systeminfo" )
368+ // FIX BUG #3: Use WMI for locale-independent parsing
369+ cmd := exec .CommandContext (ctx , "wmic" , "os" , "get" , "LastBootUpTime" , "/value" )
357370 output , err := cmd .Output ()
358371 if err != nil {
359372 return 0 , err
360373 }
374+
375+ // Parse: LastBootUpTime=20251024153045.500000+060
361376 lines := strings .Split (string (output ), "\n " )
362377 for _ , line := range lines {
363- if strings .Contains (line , "System Boot Time" ) {
364- bootTimeStr := strings .TrimSpace (strings .Split (line , ":" )[1 ])
365- bootTime , err := time .Parse ("1/2/2006, 3:04:05 PM" , bootTimeStr )
366- if err != nil {
367- return 0 , err
378+ if strings .HasPrefix (line , "LastBootUpTime=" ) {
379+ timeStr := strings .TrimPrefix (line , "LastBootUpTime=" )
380+ timeStr = strings .TrimSpace (timeStr )
381+ // Parse WMI datetime format: YYYYMMDDHHmmss.mmmmmm±UUU
382+ if len (timeStr ) >= 14 {
383+ timeStr = timeStr [:14 ] // YYYYMMDDHHmmss
384+ bootTime , err := time .Parse ("20060102150405" , timeStr )
385+ if err != nil {
386+ return 0 , err
387+ }
388+ return int64 (time .Since (bootTime ).Seconds ()), nil
368389 }
369- return int64 (time .Since (bootTime ).Seconds ()), nil
370390 }
371391 }
372392 return 0 , fmt .Errorf ("could not determine system uptime" )
373393
374394 case "darwin" :
375- cmd := exec .Command ( "sysctl" , "-n" , "kern.boottime" )
395+ cmd := exec .CommandContext ( ctx , "sysctl" , "-n" , "kern.boottime" )
376396 output , err := cmd .Output ()
377397 if err != nil {
378398 return 0 , err
@@ -394,7 +414,7 @@ func getUptime() (int64, error) {
394414 return currentTime - bootTime , nil
395415
396416 default :
397- cmd := exec .Command ( "cat" , "/proc/uptime" )
417+ cmd := exec .CommandContext ( ctx , "cat" , "/proc/uptime" )
398418 output , err := cmd .Output ()
399419 if err != nil {
400420 return 0 , err
@@ -410,9 +430,8 @@ func getUptime() (int64, error) {
410430 }
411431}
412432
433+ // FIX BUG #6: More efficient parseFloat using math.Round
413434func parseFloat (val float64 , precision int ) float64 {
414- format := fmt .Sprintf ("%%.%df" , precision )
415- formatted := fmt .Sprintf (format , val )
416- result , _ := strconv .ParseFloat (formatted , 64 )
417- return result
435+ multiplier := math .Pow (10 , float64 (precision ))
436+ return math .Round (val * multiplier ) / multiplier
418437}
0 commit comments