Skip to content

Commit 85e94e6

Browse files
authored
Merge pull request #1 from hhftechnology/main
[pull] main from hhftechnology:main
2 parents 07d4375 + fba3f49 commit 85e94e6

File tree

25 files changed

+3717
-2045
lines changed

25 files changed

+3717
-2045
lines changed

.github/workflows/build-and-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,4 +413,4 @@ jobs:
413413
echo "- [GitHub Packages](https://github.com/${{ github.repository }}/pkgs/container)" >> $GITHUB_STEP_SUMMARY
414414
if [[ "${{ github.ref_type }}" == "tag" ]]; then
415415
echo "- [Release Page](https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }})" >> $GITHUB_STEP_SUMMARY
416-
fi
416+
fi

README.md

Lines changed: 496 additions & 917 deletions
Large diffs are not rendered by default.

agent/pkg/system/system.go

Lines changed: 114 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package system
22

33
import (
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
1822
type 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
4549
type 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

6156
func 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

150153
func 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

199211
func 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

234248
func 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
294335
func 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-
353362
func 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
413434
func 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

Comments
 (0)